diff --git a/web/src/app.css b/web/src/app.css
index 687a105d2c..8d716f175e 100644
--- a/web/src/app.css
+++ b/web/src/app.css
@@ -257,6 +257,304 @@
animation: none;
align-content: center;
}
+ ::view-transition-old(memory-overlay),
+ ::view-transition-old(memory-controls),
+ ::view-transition-new(memory-overlay),
+ ::view-transition-new(memory-controls) {
+ width: 100%;
+ height: 100%;
+ object-fit: none;
+ object-position: left top;
+ }
+
+ html:active-view-transition-type(memory) {
+ &::view-transition-group(hero),
+ &::view-transition-group(hero-out) {
+ animation-duration: var(--vt-duration-memory);
+ animation-timing-function: var(--vt-memory-easing);
+ overflow: hidden;
+ z-index: 1;
+ }
+ &::view-transition-group(memory-overlay),
+ &::view-transition-group(memory-controls) {
+ animation: none;
+ z-index: 5;
+ }
+ &::view-transition-group(memory-overlay-prev),
+ &::view-transition-group(memory-overlay-next) {
+ animation: none;
+ z-index: 2;
+ opacity: 0.25;
+ }
+ &::view-transition-image-pair(memory-overlay),
+ &::view-transition-image-pair(memory-controls) {
+ isolation: auto;
+ }
+ &::view-transition-old(memory-overlay),
+ &::view-transition-old(memory-controls) {
+ animation: 120ms linear fadeOut forwards;
+ }
+ &::view-transition-new(memory-overlay),
+ &::view-transition-new(memory-controls) {
+ animation: 200ms linear calc(var(--vt-duration-memory) - 200ms) fadeIn forwards;
+ opacity: 0;
+ }
+ &::view-transition-old(memory-overlay-prev),
+ &::view-transition-old(memory-overlay-next) {
+ display: none;
+ }
+ &::view-transition-new(memory-overlay-prev),
+ &::view-transition-new(memory-overlay-next) {
+ animation: none;
+ width: 100%;
+ height: 100%;
+ object-fit: none;
+ object-position: left top;
+ }
+ &::view-transition-image-pair(hero) {
+ isolation: auto;
+ }
+ &::view-transition-old(hero) {
+ display: none;
+ }
+ &::view-transition-new(hero) {
+ animation: none;
+ object-fit: cover;
+ width: 100%;
+ height: 100%;
+ }
+ &::view-transition-image-pair(hero-out) {
+ isolation: auto;
+ }
+ &::view-transition-old(hero-out) {
+ display: none;
+ }
+ &::view-transition-new(hero-out) {
+ animation: var(--vt-duration-memory) var(--vt-memory-easing) dimDown forwards;
+ object-fit: cover;
+ width: 100%;
+ height: 100%;
+ }
+ &::view-transition-group(memory-departing) {
+ animation: none;
+ }
+ &::view-transition-old(memory-departing) {
+ animation: calc(var(--vt-duration-memory) * 0.4) linear fadeFromDim forwards;
+ }
+ &::view-transition-new(memory-departing) {
+ animation: none;
+ visibility: hidden;
+ }
+ }
+
+ html:active-view-transition-type(memory-enter) {
+ &::view-transition-group(hero) {
+ animation-duration: var(--vt-duration-hero);
+ animation-timing-function: var(--vt-memory-easing);
+ overflow: hidden;
+ }
+ &::view-transition-old(hero),
+ &::view-transition-new(hero) {
+ animation: none;
+ object-fit: cover;
+ width: 100%;
+ height: 100%;
+ }
+ &::view-transition-group(memory-overlay),
+ &::view-transition-group(memory-controls),
+ &::view-transition-group(memory-nav-buttons) {
+ animation: none;
+ z-index: 5;
+ }
+ &::view-transition-old(memory-overlay),
+ &::view-transition-old(memory-controls),
+ &::view-transition-old(memory-nav-buttons) {
+ animation: none;
+ visibility: hidden;
+ }
+ &::view-transition-new(memory-overlay),
+ &::view-transition-new(memory-controls),
+ &::view-transition-new(memory-nav-buttons) {
+ animation: 200ms linear var(--vt-duration-hero) fadeIn forwards;
+ opacity: 0;
+ }
+ }
+
+ ::view-transition-old(memory-fade-out) {
+ animation: 500ms linear crossfadeOut forwards;
+ }
+ ::view-transition-new(memory-fade-in) {
+ animation: 500ms linear crossfadeIn forwards;
+ }
+
+ html:active-view-transition-type(memory-nav-fast) {
+ &::view-transition-old(memory-fade-out) {
+ animation-duration: 250ms;
+ }
+ &::view-transition-new(memory-fade-in) {
+ animation-duration: 250ms;
+ }
+ &::view-transition-old(memory-overlay),
+ &::view-transition-old(memory-controls) {
+ animation-duration: 100ms;
+ }
+ &::view-transition-new(memory-overlay),
+ &::view-transition-new(memory-controls) {
+ animation: 100ms linear 150ms fadeIn forwards;
+ opacity: 0;
+ }
+ }
+
+ html:active-view-transition-type(memory-nav) {
+ &::view-transition-group(memory-overlay),
+ &::view-transition-group(memory-controls) {
+ animation: none;
+ z-index: 5;
+ }
+ &::view-transition-image-pair(memory-overlay),
+ &::view-transition-image-pair(memory-controls) {
+ isolation: auto;
+ }
+ &::view-transition-old(memory-overlay),
+ &::view-transition-old(memory-controls) {
+ animation: 150ms linear fadeOut forwards;
+ }
+ &::view-transition-new(memory-overlay),
+ &::view-transition-new(memory-controls) {
+ animation: 200ms linear 300ms fadeIn forwards;
+ opacity: 0;
+ }
+ &::view-transition-group(memory-overlay-prev),
+ &::view-transition-group(memory-overlay-next) {
+ animation: none;
+ opacity: 0.25;
+ }
+ &::view-transition-old(memory-overlay-prev),
+ &::view-transition-old(memory-overlay-next) {
+ display: none;
+ }
+ &::view-transition-new(memory-overlay-prev),
+ &::view-transition-new(memory-overlay-next) {
+ animation: none;
+ }
+ }
+
+ ::view-transition-old(next),
+ ::view-transition-old(next-old),
+ ::view-transition-new(next),
+ ::view-transition-new(next-new),
+ ::view-transition-old(previous),
+ ::view-transition-old(previous-old),
+ ::view-transition-new(previous),
+ ::view-transition-new(previous-new) {
+ animation-duration: var(--vt-duration-viewer-navigation);
+ animation-timing-function: var(--vt-viewer-slide-easing);
+ animation-fill-mode: forwards;
+ }
+
+ ::view-transition-old(next),
+ ::view-transition-old(next-old),
+ ::view-transition-old(previous),
+ ::view-transition-old(previous-old) {
+ opacity: var(--vt-viewer-old-opacity);
+ }
+
+ ::view-transition-old(next),
+ ::view-transition-old(next-old) {
+ animation-name: var(--vt-viewer-next-out);
+ }
+
+ ::view-transition-new(next),
+ ::view-transition-new(next-new) {
+ animation-name: var(--vt-viewer-next-in);
+ }
+
+ ::view-transition-old(previous),
+ ::view-transition-old(previous-old) {
+ animation-name: var(--vt-viewer-prev-out);
+ }
+
+ ::view-transition-new(previous),
+ ::view-transition-new(previous-new) {
+ animation-name: var(--vt-viewer-prev-in);
+ }
+
+ ::view-transition-old(next-old),
+ ::view-transition-new(next-new),
+ ::view-transition-old(previous-old),
+ ::view-transition-new(previous-new) {
+ overflow: hidden;
+ }
+
+ ::view-transition-old(previous-old) {
+ z-index: -1;
+ }
+
+ @keyframes fadeFromDim {
+ from {
+ opacity: 0.25;
+ }
+ to {
+ opacity: 0;
+ }
+ }
+
+ @keyframes dimDown {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0.25;
+ }
+ }
+
+ @keyframes flyInLeft {
+ from {
+ transform: translateX(calc(-1 * var(--vt-viewer-slide-distance)));
+ opacity: var(--vt-viewer-opacity-start);
+ filter: blur(var(--vt-viewer-blur-max));
+ }
+ to {
+ opacity: 1;
+ filter: blur(0);
+ }
+ }
+
+ @keyframes flyOutLeft {
+ from {
+ opacity: 1;
+ filter: blur(0);
+ }
+ to {
+ transform: translateX(calc(-1 * var(--vt-viewer-slide-distance)));
+ opacity: var(--vt-viewer-opacity-start);
+ filter: blur(var(--vt-viewer-blur-max));
+ }
+ }
+
+ @keyframes flyInRight {
+ from {
+ transform: translateX(var(--vt-viewer-slide-distance));
+ opacity: var(--vt-viewer-opacity-start);
+ filter: blur(var(--vt-viewer-blur-max));
+ }
+ to {
+ opacity: 1;
+ filter: blur(0);
+ }
+ }
+
+ @keyframes flyOutRight {
+ from {
+ opacity: 1;
+ filter: blur(0);
+ }
+ to {
+ transform: translateX(var(--vt-viewer-slide-distance));
+ opacity: var(--vt-viewer-opacity-start);
+ filter: blur(var(--vt-viewer-blur-max));
+ }
+ }
@keyframes panelSlideInRight {
from {
@@ -331,5 +629,69 @@
animation: var(--vt-duration-default) 0s fadeIn forwards;
}
}
+
+ ::view-transition-group(letterbox-left),
+ ::view-transition-group(letterbox-right),
+ ::view-transition-group(letterbox-top),
+ ::view-transition-group(letterbox-bottom) {
+ z-index: 100;
+ }
+
+ ::view-transition-old(letterbox-left),
+ ::view-transition-old(letterbox-right),
+ ::view-transition-old(letterbox-top),
+ ::view-transition-old(letterbox-bottom),
+ ::view-transition-new(letterbox-left),
+ ::view-transition-new(letterbox-right),
+ ::view-transition-new(letterbox-top),
+ ::view-transition-new(letterbox-bottom) {
+ background-color: transparent;
+ }
+
+ html:active-view-transition-type(viewer-nav) {
+ &::view-transition-group(previous),
+ &::view-transition-group(previous-old),
+ &::view-transition-group(next),
+ &::view-transition-group(next-old) {
+ width: 100% !important;
+ height: 100% !important;
+ transform: none !important;
+ }
+
+ &::view-transition-old(previous),
+ &::view-transition-old(previous-old),
+ &::view-transition-old(next),
+ &::view-transition-old(next-old) {
+ animation: var(--vt-duration-viewer-navigation) fadeOut forwards;
+ transform-origin: center;
+ height: 100%;
+ width: 100%;
+ object-fit: contain;
+ overflow: hidden;
+ }
+
+ &::view-transition-new(previous),
+ &::view-transition-new(previous-new),
+ &::view-transition-new(next),
+ &::view-transition-new(next-new) {
+ animation: var(--vt-duration-viewer-navigation) fadeIn forwards;
+ transform-origin: center;
+ height: 100%;
+ width: 100%;
+ object-fit: contain;
+ }
+ }
+
+ html:active-view-transition-type(memory-enter) {
+ &::view-transition-group(hero) {
+ animation-duration: 0s;
+ }
+ &::view-transition-old(hero) {
+ animation: var(--vt-duration-default) fadeOut forwards;
+ }
+ &::view-transition-new(hero) {
+ animation: var(--vt-duration-default) fadeIn forwards;
+ }
+ }
}
}
diff --git a/web/src/lib/components/memory-page/memory-photo-viewer.svelte b/web/src/lib/components/memory-page/memory-photo-viewer.svelte
index e69f31fbd0..b37da23dba 100644
--- a/web/src/lib/components/memory-page/memory-photo-viewer.svelte
+++ b/web/src/lib/components/memory-page/memory-photo-viewer.svelte
@@ -1,57 +1,32 @@
-{#if !imageLoaded}
-
-
-{/if}
-
-{#if !imageLoaded}
-
-{:else if imageLoaded}
-
-

-
-{/if}
+
+ {#if containerWidth > 0 && containerHeight > 0}
+
+ {:else}
+
+ {/if}
+
diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte
index 0b6f89ec5c..73a4ae1392 100644
--- a/web/src/lib/components/memory-page/memory-viewer.svelte
+++ b/web/src/lib/components/memory-page/memory-viewer.svelte
@@ -22,10 +22,16 @@
import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
+ import { eventManager } from '$lib/managers/event-manager.svelte';
import { memoryManager, type MemoryAsset } from '$lib/managers/memory-manager.svelte';
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
+ import { viewTransitionManager } from '$lib/managers/ViewTransitionManager.svelte';
import { Route } from '$lib/route';
import { getAssetBulkActions } from '$lib/services/asset.service';
+ import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
+ import { assetViewingStore } from '$lib/stores/asset-viewing.store';
+ import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte';
+ import { memoryStore, type MemoryAsset } from '$lib/stores/memory.store.svelte';
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
import { getAssetMediaUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils';
import { fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util';
@@ -50,6 +56,7 @@
} from '@mdi/js';
import type { NavigationTarget, Page } from '@sveltejs/kit';
import { DateTime } from 'luxon';
+ import { tick } from 'svelte';
import { t } from 'svelte-i18n';
import type { Attachment } from 'svelte/attachments';
import { Tween } from 'svelte/motion';
@@ -62,6 +69,7 @@
let paused = $state(false);
let current = $state(undefined);
const currentAssetId = $derived(current?.asset.id);
+ const currentAssetDto = $derived(current ? current.memory.assets[current.assetIndex] : undefined);
const currentMemoryAssetFull = $derived.by(async () =>
currentAssetId ? await getAssetInfo({ ...authManager.params, id: currentAssetId }) : undefined,
);
@@ -74,6 +82,14 @@
let isSaved = $derived(current?.memory.isSaved);
let viewerHeight = $state(0);
+ let transition = $state({
+ name: undefined as string | undefined,
+ previousPanel: undefined as string | undefined,
+ nextPanel: undefined as string | undefined,
+ active: false,
+ });
+ const showTransitionOverlays = $derived(transition.active || transition.name === 'hero');
+ const showNavButtonOverlay = $derived(transition.name === 'hero');
const viewport: Viewport = $state({ width: 0, height: 0 });
// need to include padding in the viewport for gallery
@@ -82,18 +98,6 @@
let videoPlayer: HTMLVideoElement | undefined = $state();
const asHref = (asset: { id: string }) => `?${QueryParameter.ID}=${asset.id}`;
- const handleNavigate = async (asset?: { id: string }) => {
- if (assetViewerManager.isViewing) {
- return asset;
- }
-
- if (!asset) {
- return;
- }
-
- await goto(asHref(asset));
- };
-
const setProgressDuration = (asset: TimelineAsset) => {
if (asset.isVideo) {
const timeParts = asset.duration!.split(':').map(Number);
@@ -109,11 +113,177 @@
}
};
- 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(Route.photos());
+ const scrollToTop = () => {
+ if (window.scrollY === 0) {
+ return Promise.resolve();
+ }
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ return new Promise((resolve) => {
+ const timeout = setTimeout(resolve, 500);
+ window.addEventListener(
+ 'scrollend',
+ () => {
+ clearTimeout(timeout);
+ resolve();
+ },
+ { once: true },
+ );
+ });
+ };
+
+ const withMemoryTransition = async (
+ asset: { id: string } | undefined,
+ config: Omit[0], 'onFinished'> & {
+ onFinished?: () => void;
+ },
+ ) => {
+ if ($isViewing || !asset) {
+ return;
+ }
+
+ await scrollToTop();
+
+ transition.active = true;
+ viewTransitionManager
+ .startTransition({
+ ...config,
+ onFinished: () => {
+ transition.previousPanel = undefined;
+ transition.nextPanel = undefined;
+ transition.name = undefined;
+ transition.active = false;
+ config.onFinished?.();
+ },
+ })
+ .catch((error: unknown) => console.error('[Memory] transition failed:', error));
+ };
+
+ const navigateWithTransition = (asset?: { id: string }) =>
+ withMemoryTransition(asset, {
+ types: ['memory-nav'],
+ prepareOldSnapshot: () => {
+ transition.name = 'memory-fade-out';
+ },
+ performUpdate: async () => {
+ await goto(asHref(asset!));
+ await eventManager.untilNext('ViewerOpenTransitionReady');
+ },
+ prepareNewSnapshot: () => {
+ transition.name = 'memory-fade-in';
+ },
+ });
+
+ const handleNextAsset = () => {
+ const next = current?.next;
+ if (next && next.memory.id !== current?.memory.id) {
+ void navigateToMemory('next', next.asset);
+ } else {
+ void navigateWithTransition(next?.asset);
+ }
+ };
+ const handlePreviousAsset = () => {
+ const previous = current?.previous;
+ if (previous && previous.memory.id !== current?.memory.id) {
+ void navigateToMemory('previous', previous.asset);
+ } else {
+ void navigateWithTransition(previous?.asset);
+ }
+ };
+ const navigateToMemory = (direction: 'next' | 'previous', asset?: { id: string }) => {
+ const isNext = direction === 'next';
+ const useHeroMorph = !mediaQueryManager.reducedMotion;
+
+ return withMemoryTransition(asset, {
+ types: ['memory'],
+ prepareOldSnapshot: () => {
+ if (useHeroMorph) {
+ if (isNext) {
+ transition.nextPanel = 'hero';
+ transition.previousPanel = 'memory-departing';
+ } else {
+ transition.previousPanel = 'hero';
+ transition.nextPanel = 'memory-departing';
+ }
+ transition.name = 'hero-out';
+ } else {
+ transition.name = 'memory-fade-out';
+ }
+ },
+ performUpdate: async () => {
+ transition.nextPanel = undefined;
+ transition.previousPanel = undefined;
+ if (useHeroMorph) {
+ if (isNext) {
+ transition.previousPanel = 'hero-out';
+ } else {
+ transition.nextPanel = 'hero-out';
+ }
+ }
+ transition.name = useHeroMorph ? 'hero' : 'memory-fade-in';
+ await goto(asHref(asset!));
+ await eventManager.untilNext('ViewerOpenTransitionReady');
+ },
+ });
+ };
+
+ const handleNextMemory = () => void navigateToMemory('next', current?.nextMemory?.assets[0]);
+ const handlePreviousMemory = () => void navigateToMemory('previous', current?.previousMemory?.assets[0]);
+ const closeMemoryViewer = () => {
+ if (current && current.assetIndex > 0 && !mediaQueryManager.reducedMotion) {
+ const firstAsset = current.memory.assets[0];
+ void withMemoryTransition(firstAsset, {
+ types: ['memory-nav', 'memory-nav-fast'],
+ prepareOldSnapshot: () => {
+ transition.name = 'memory-fade-out';
+ },
+ performUpdate: async () => {
+ await goto(asHref(firstAsset));
+ await eventManager.untilNext('ViewerOpenTransitionReady');
+ },
+ prepareNewSnapshot: () => {
+ transition.name = 'memory-fade-in';
+ },
+ onFinished: () => closeToTimeline(),
+ });
+ } else {
+ closeToTimeline();
+ }
+ };
+
+ const closeToTimeline = () => {
+ const memoryId = current?.memory.id;
+ let cardImage: HTMLElement | null | undefined;
+
+ void viewTransitionManager.startTransition({
+ types: ['memory-enter'],
+ prepareOldSnapshot: () => {
+ transition.name = 'hero';
+ },
+ performUpdate: async () => {
+ transition.name = undefined;
+ await goto(Route.photos());
+ await tick();
+
+ const memoryCard = memoryId
+ ? document.querySelector(`[data-memory-id="${CSS.escape(memoryId)}"]`)
+ : null;
+ memoryCard?.scrollIntoView({ behavior: 'instant', inline: 'nearest', block: 'nearest' });
+ cardImage = memoryCard?.querySelector('img');
+ if (cardImage) {
+ cardImage.style.viewTransitionName = 'hero';
+ await tick();
+ }
+ },
+ onFinished: () => {
+ if (cardImage) {
+ cardImage.style.viewTransitionName = '';
+ cardImage = null;
+ }
+ },
+ });
+ };
+
+ const handleEscape = closeMemoryViewer;
const handleSelectAll = () =>
assetMultiSelectManager.selectAssets(current?.memory.assets.map((a) => toTimelineAsset(a)) || []);
@@ -157,13 +327,17 @@
}
};
- const handleProgress = async (progress: number) => {
+ const handleProgress = (progress: number) => {
if (!progressBarController) {
return;
}
- if (progress === 1 && !paused) {
- await (current?.next ? handleNextAsset() : handlePromiseError(handleAction('handleProgressLast', 'pause')));
+ if (progress === 1 && !paused && !transition.active) {
+ if (current?.next) {
+ handleNextAsset();
+ } else {
+ handlePromiseError(handleAction('handleProgressLast', 'pause'));
+ }
}
};
@@ -267,7 +441,18 @@
playerInitialized = false;
};
- const resetAndPlay = () => {
+ const resolveTransitionIfPending = () => {
+ if (viewTransitionManager.activeViewTransition) {
+ transition.name = 'hero';
+ eventManager.emit('ViewerOpenTransitionReady');
+ requestAnimationFrame(() => {
+ transition.name = undefined;
+ });
+ }
+ };
+
+ const handleMemoryImageReady = () => {
+ resolveTransitionIfPending();
handlePromiseError(handleAction('resetAndPlay', 'reset'));
handlePromiseError(handleAction('resetAndPlay', 'play'));
};
@@ -282,7 +467,7 @@
handlePromiseError(handleAction('initPlayer[AssetViewOpen]', 'pause'));
} else if (isVideo) {
// Image assets will start playing when the image is loaded. Only autostart video assets.
- resetAndPlay();
+ handleMemoryImageReady();
}
playerInitialized = true;
};
@@ -310,7 +495,7 @@
$effect(() => {
if (progressBarController) {
- handlePromiseError(handleProgress(progressBarController.current));
+ handleProgress(progressBarController.current);
}
});
@@ -379,7 +564,7 @@
bind:clientWidth={viewport.width}
>
{#if current}
- goto(Route.photos())} forceDark multiRow>
+
{#snippet leading()}
{#if current}
@@ -455,7 +640,11 @@
class="ms-[-100%] box-border flex h-[calc(100vh-224px)] md:h-[calc(100vh-180px)] w-[300%] items-center justify-center gap-10 overflow-hidden"
>
-
+
{:else}
+
{$t('previous')}
{$memoryLaneTitle(current.previousMemory)}
@@ -489,39 +682,42 @@
-
-
- {#key current.asset.id}
- {#if current.asset.isVideo}
-
- {:else}
-
- {/if}
- {/key}
+
+ {#key current.asset.id}
+ {#if current.asset.isVideo}
+
+ {:else if currentAssetDto}
+
+ {/if}
+ {/key}
-
-
- handleSaveMemory()}
- class="w-12 h-12"
- />
-
- handlePromiseError(handleAction('ContextMenuClick', 'pause'))}
- direction="left"
- size="medium"
- align="bottom-right"
- >
- handleDeleteMemory()} text={$t('remove_memory')} icon={mdiCardsOutline} />
- handleDeleteMemoryAsset()}
- text={$t('remove_photo_from_memory')}
- icon={mdiImageMinusOutline}
- />
-
-
-
-
-
- {#await currentMemoryAssetFull then asset}
- {#if asset}
-
- {/if}
- {/await}
-
+
handlePromiseError(handleAction('ContextMenuClick', 'pause'))}
+ direction="left"
+ size="medium"
+ align="bottom-right"
+ >
+ handleDeleteMemory()} text={$t('remove_memory')} icon={mdiCardsOutline} />
+ handleDeleteMemoryAsset()}
+ text={$t('remove_photo_from_memory')}
+ icon={mdiImageMinusOutline}
+ />
+
+
-
+
+
+ {#await currentMemoryAssetFull then asset}
+ {#if asset}
+
+ {/if}
+ {/await}
+
+
+
+
{#if current.previous}
-
+
-
-
- {fromISODateTimeUTC(current.memory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL, {
- locale: $locale,
- })}
-
-
- {#await currentMemoryAssetFull then asset}
- {asset?.exifInfo?.city || ''}
- {asset?.exifInfo?.country || ''}
- {/await}
-
-
+
+
+ {fromISODateTimeUTC(current.memory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL, {
+ locale: $locale,
+ })}
+
+
+ {#await currentMemoryAssetFull then asset}
+ {asset?.exifInfo?.city || ''}
+ {asset?.exifInfo?.country || ''}
+ {/await}
+
-
+
{:else}
+
{$t('up_next')}
{$memoryLaneTitle(current.nextMemory)}
@@ -677,8 +888,6 @@
diff --git a/web/src/lib/utils/transition-utils.ts b/web/src/lib/utils/transition-utils.ts
index 323f489d23..a29645b31d 100644
--- a/web/src/lib/utils/transition-utils.ts
+++ b/web/src/lib/utils/transition-utils.ts
@@ -2,14 +2,15 @@ import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { viewTransitionManager } from '$lib/managers/ViewTransitionManager.svelte';
import { tick } from 'svelte';
-export function startViewerTransition(
+function startHeroTransition(
+ type: string,
heroAssetId: string,
openViewer: () => void,
activateHeroAsset: (assetId: string) => void,
deactivateHeroAsset: () => void,
) {
void viewTransitionManager.startTransition({
- types: ['viewer'],
+ types: [type],
prepareOldSnapshot: () => {
activateHeroAsset(heroAssetId);
},
@@ -24,6 +25,24 @@ export function startViewerTransition(
});
}
+export function startViewerTransition(
+ heroAssetId: string,
+ openViewer: () => void,
+ activateHeroAsset: (assetId: string) => void,
+ deactivateHeroAsset: () => void,
+) {
+ startHeroTransition('viewer', heroAssetId, openViewer, activateHeroAsset, deactivateHeroAsset);
+}
+
+export function startMemoryTransition(
+ heroAssetId: string,
+ openViewer: () => void,
+ activateHeroAsset: (assetId: string) => void,
+ deactivateHeroAsset: () => void,
+) {
+ startHeroTransition('memory-enter', heroAssetId, openViewer, activateHeroAsset, deactivateHeroAsset);
+}
+
let activeOverlay: HTMLElement | undefined;
export function removeCrossfadeOverlay() {
diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
index d8f4550693..12bb5a7810 100644
--- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
@@ -1,4 +1,5 @@
@@ -103,7 +116,33 @@
withStacked
>
{#if authManager.preferences.memories.enabled}
-
+ {#snippet memoryCard(item: CarouselImageItem)}
+ {
+ e.preventDefault();
+ handleMemoryCardClick(item);
+ }}
+ style:box-shadow="rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 1px 3px 1px"
+ >
+
+
+
+ {item.title}
+
+
+ {/snippet}
+
{/if}
{#snippet empty()}
openFileUploadDialog()} class="mt-10 mx-auto" />