mirror of
https://github.com/immich-app/immich.git
synced 2025-01-16 16:56:46 +01:00
parent
cdbc673a59
commit
a373d50c31
4 changed files with 242 additions and 180 deletions
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in a new issue