diff --git a/web/src/lib/components/album-page/album-map.svelte b/web/src/lib/components/album-page/album-map.svelte index fbb831a38b..bb500f9a5e 100644 --- a/web/src/lib/components/album-page/album-map.svelte +++ b/web/src/lib/components/album-page/album-map.svelte @@ -1,7 +1,8 @@ diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 0866d38557..5ad118a3f5 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -4,9 +4,9 @@ import AlbumMap from '$lib/components/album-page/album-map.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; + import type { AssetManager } from '$lib/managers/asset-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; - import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { handlePromiseError } from '$lib/utils'; @@ -25,16 +25,15 @@ import AlbumSummary from './album-summary.svelte'; interface Props { + assetManager: AssetManager; sharedLink: SharedLinkResponseDto; user?: UserResponseDto | undefined; } - let { sharedLink, user = undefined }: Props = $props(); + let { assetManager = $bindable(), sharedLink, user = undefined }: Props = $props(); const album = sharedLink.album as AlbumResponseDto; - let { isViewing: showAssetViewer } = assetViewingStore; - const timelineManager = new TimelineManager(); $effect(() => void timelineManager.updateOptions({ albumId: album.id, order: album.order })); onDestroy(() => timelineManager.destroy()); @@ -53,7 +52,7 @@ use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: () => { - if (!$showAssetViewer && assetInteraction.selectionActive) { + if (!assetManager.showAssetViewer && assetInteraction.selectionActive) { cancelMultiselect(assetInteraction); } }, @@ -61,7 +60,7 @@ />
- +

{/if} {#if sharedLink.showMetadata && $featureFlags.loaded && $featureFlags.map} - + {/if} {/snippet} diff --git a/web/src/lib/components/asset-viewer/actions/download-action.svelte b/web/src/lib/components/asset-viewer/actions/download-action.svelte index e6c96da016..9d035f81a8 100644 --- a/web/src/lib/components/asset-viewer/actions/download-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/download-action.svelte @@ -1,22 +1,20 @@ diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index a376c37139..95a22ef0d0 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -21,6 +21,7 @@ import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import { AppRoute } from '$lib/constants'; + import type { AssetManager } from '$lib/managers/asset-manager.svelte'; import { user } from '$lib/stores/user.store'; import { photoZoomState } from '$lib/stores/zoom-image.store'; import { getAssetJobName, getSharedLink } from '$lib/utils'; @@ -32,7 +33,6 @@ AssetTypeEnum, AssetVisibility, type AlbumResponseDto, - type AssetResponseDto, type PersonResponseDto, type StackResponseDto, } from '@immich/sdk'; @@ -55,7 +55,7 @@ import { t } from 'svelte-i18n'; interface Props { - asset: AssetResponseDto; + assetManager: AssetManager; album?: AlbumResponseDto | null; person?: PersonResponseDto | null; stack?: StackResponseDto | null; @@ -75,7 +75,7 @@ } let { - asset, + assetManager = $bindable(), album = null, person = null, stack = null, @@ -93,6 +93,8 @@ motionPhoto, }: Props = $props(); + let asset = $derived(assetManager.asset); + const sharedLink = getSharedLink(); let isOwner = $derived($user && asset.ownerId === $user?.id); let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline); @@ -158,7 +160,7 @@ {/if} {#if !isOwner && showDownloadButton} - + {/if} {#if showDetailButton} @@ -177,7 +179,7 @@ {/if} {#if showDownloadButton} - + {/if} {#if !isLocked} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 4f793e7b52..6d6e25a262 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -7,22 +7,21 @@ import AssetViewerNavBar from '$lib/components/asset-viewer/asset-viewer-nav-bar.svelte'; import { AssetAction, ProjectionType } from '$lib/constants'; import { activityManager } from '$lib/managers/activity-manager.svelte'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; - import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import { closeEditorCofirm } from '$lib/stores/asset-editor.store'; - import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { isShowDetail } from '$lib/stores/preferences.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { user } from '$lib/stores/user.store'; import { websocketEvents } from '$lib/stores/websocket'; import { getAssetJobMessage, getSharedLink, handlePromiseError } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; + import { navigate } from '$lib/utils/navigation'; import { SlideshowHistory } from '$lib/utils/slideshow-history'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { AssetJobName, AssetTypeEnum, - getAllAlbums, getStack, runAssetJobs, type AlbumResponseDto, @@ -48,8 +47,7 @@ type HasAsset = boolean; interface Props { - asset: AssetResponseDto; - preloadAssets?: TimelineAsset[]; + assetManager: AssetManager; showNavigation?: boolean; withStacked?: boolean; isShared?: boolean; @@ -61,13 +59,12 @@ onClose: (asset: AssetResponseDto) => void; onNext: () => Promise; onPrevious: () => Promise; - onRandom: () => Promise<{ id: string } | undefined>; + onRandom: () => Promise; copyImage?: () => Promise; } let { - asset = $bindable(), - preloadAssets = $bindable([]), + assetManager = $bindable(), showNavigation = true, withStacked = false, isShared = false, @@ -83,7 +80,6 @@ copyImage = $bindable(), }: Props = $props(); - const { setAssetId } = assetViewingStore; const { restartProgress: restartSlideshowProgress, stopProgress: stopSlideshowProgress, @@ -94,10 +90,13 @@ const stackThumbnailSize = 60; const stackSelectedThumbnailSize = 65; - let appearsInAlbums: AlbumResponseDto[] = $state([]); + let asset = $derived(assetManager.asset); + let preloadAssets = $derived(assetManager.preloadAssets); + let albums = $derived(assetManager.albums); + let shouldPlayMotionPhoto = $state(false); let sharedLink = getSharedLink(); - let enableDetailPanel = asset.hasMetadata; + let enableDetailPanel = $derived(asset.hasMetadata); let slideshowStateUnsubscribe: () => void; let shuffleSlideshowUnsubscribe: () => void; let previewStackedAsset: AssetResponseDto | undefined = $state(); @@ -146,7 +145,7 @@ } }; - onMount(async () => { + onMount(() => { unsubscribes.push( websocketEvents.on('on_upload_success', (asset) => onAssetUpdate({ event: 'upload', asset })), websocketEvents.on('on_asset_update', (asset) => onAssetUpdate({ event: 'update', asset })), @@ -169,9 +168,7 @@ } }); - if (!sharedLink) { - await handleGetAllAlbums(); - } + // TODO: empty shared link returns 404. }); onDestroy(() => { @@ -190,18 +187,6 @@ activityManager.reset(); }); - const handleGetAllAlbums = async () => { - if (authManager.key) { - return; - } - - try { - appearsInAlbums = await getAllAlbums({ assetId: asset.id }); - } catch (error) { - console.error('Error getting album that asset belong to', error); - } - }; - const handleOpenActivity = () => { if ($isShowDetail) { $isShowDetail = false; @@ -238,11 +223,11 @@ let hasNext = false; if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) { - hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next(); + hasNext = order === 'previous' ? await slideshowHistory.previous() : await slideshowHistory.next(); if (!hasNext) { - const asset = await onRandom(); - if (asset) { - slideshowHistory.queue(asset); + await onRandom(); + if (assetManager.asset) { + slideshowHistory.queue(assetManager.asset); hasNext = true; } } @@ -281,8 +266,9 @@ let assetViewerHtmlElement = $state(); - const slideshowHistory = new SlideshowHistory((asset) => { - handlePromiseError(setAssetId(asset.id).then(() => ($restartSlideshowProgress = true))); + const slideshowHistory = new SlideshowHistory(async (asset) => { + await navigate({ targetRoute: 'current', assetId: asset.id }); + $restartSlideshowProgress = true; }); const handleVideoStarted = () => { @@ -325,7 +311,7 @@ const handleAction = async (action: Action) => { switch (action.type) { case AssetAction.ADD_TO_ALBUM: { - await handleGetAllAlbums(); + await assetManager.refreshAlbums(); break; } case AssetAction.SET_STACK_PRIMARY_ASSET: { @@ -364,11 +350,6 @@ handlePromiseError(activityManager.init(album.id, asset.id)); } }); - $effect(() => { - if (asset.id) { - handlePromiseError(handleGetAllAlbums()); - } - }); @@ -383,7 +364,7 @@ {#if $slideshowState === SlideshowState.None && !isShowEditor}
- ($isShowDetail = false)} /> + ($isShowDetail = false)} />
{/if} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index ef4ddc13ce..38dd23f2ab 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -13,7 +13,7 @@ import { locale } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { preferences, user } from '$lib/stores/user.store'; - import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils'; + import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils'; import { delay, isFlipped } from '$lib/utils/asset-utils'; import { getByteUnitString } from '$lib/utils/byte-units'; import { handleError } from '$lib/utils/handle-error'; @@ -22,7 +22,6 @@ import { getParentPath } from '$lib/utils/tree-utils'; import { AssetMediaSize, - getAssetInfo, updateAsset, type AlbumResponseDto, type AssetResponseDto, @@ -83,19 +82,6 @@ let isOwner = $derived($user?.id === asset.ownerId); - const handleNewAsset = async (newAsset: AssetResponseDto) => { - // TODO: check if reloading asset data is necessary - if (newAsset.id && !authManager.key) { - const data = await getAssetInfo({ id: asset.id }); - people = data?.people || []; - unassignedFaces = data?.unassignedFaces || []; - } - }; - - $effect(() => { - handlePromiseError(handleNewAsset(asset)); - }); - let latlng = $derived( (() => { const lat = asset.exifInfo?.latitude; @@ -127,11 +113,8 @@ return undefined; }; - const handleRefreshPeople = async () => { - await getAssetInfo({ id: asset.id }).then((data) => { - people = data?.people || []; - unassignedFaces = data?.unassignedFaces || []; - }); + // TODO: refresh people + const handleRefreshPeople = () => { showEditFaces = false; }; diff --git a/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte index 0336e0a74d..0f3d2d4978 100644 --- a/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte +++ b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte @@ -2,7 +2,6 @@ import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; import { notificationController } from '$lib/components/shared-components/notification/notification'; import { modalManager } from '$lib/managers/modal-manager.svelte'; - import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { getPeopleThumbnailUrl } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; @@ -303,7 +302,8 @@ }, }); - await assetViewingStore.setAssetId(assetId); + // TODO: manual tag face + // await assetViewingStore.setAssetId(assetId); } catch (error) { handleError(error, 'Error tagging face'); } finally { diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index b2e1382a57..cb2aae76fa 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -3,7 +3,6 @@ import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import { timeBeforeShowLoadingSpinner } from '$lib/constants'; import { modalManager } from '$lib/managers/modal-manager.svelte'; - import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { boundingBoxesArray } from '$lib/stores/people.store'; import { websocketEvents } from '$lib/stores/websocket'; @@ -20,6 +19,7 @@ type AssetFaceResponseDto, type PersonResponseDto, } from '@immich/sdk'; + import { IconButton } from '@immich/ui'; import { mdiAccountOff, mdiArrowLeftThin, mdiPencil, mdiRestart, mdiTrashCan } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -28,7 +28,6 @@ import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; import AssignFaceSidePanel from './assign-face-side-panel.svelte'; - import { IconButton } from '@immich/ui'; interface Props { assetId: string; @@ -184,7 +183,8 @@ peopleWithFaces = peopleWithFaces.filter((f) => f.id !== face.id); - await assetViewingStore.setAssetId(assetId); + // TODO: manual tag face + // await assetViewingStore.setAssetId(assetId); } catch (error) { handleError(error, $t('error_delete_face')); } diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index f1a15f4429..368170aa9d 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -26,10 +26,10 @@ NotificationType, } from '$lib/components/shared-components/notification/notification'; import { AppRoute, QueryParameter } from '$lib/constants'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; - import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { type MemoryAsset, memoryStore } from '$lib/stores/memory.store.svelte'; import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store'; import { preferences } from '$lib/stores/user.store'; @@ -61,6 +61,12 @@ import { t } from 'svelte-i18n'; import { Tween } from 'svelte/motion'; + interface Props { + assetManager: AssetManager; + } + + let { assetManager = $bindable() }: Props = $props(); + let memoryGallery: HTMLElement | undefined = $state(); let memoryWrapper: HTMLElement | undefined = $state(); let galleryInView = $state(false); @@ -76,7 +82,6 @@ let isSaved = $derived(current?.memory.isSaved); let viewerHeight = $state(0); - const { isViewing } = assetViewingStore; const viewport: Viewport = $state({ width: 0, height: 0 }); // need to include padding in the viewport for gallery const galleryViewport: Viewport = $derived({ height: viewport.height, width: viewport.width - 32 }); @@ -85,7 +90,7 @@ let videoPlayer: HTMLVideoElement | undefined = $state(); const asHref = (asset: { id: string }) => `?${QueryParameter.ID}=${asset.id}`; const handleNavigate = async (asset?: { id: string }) => { - if ($isViewing) { + if (assetManager.showAssetViewer) { return asset; } @@ -251,7 +256,7 @@ if (playerInitialized || isVideoAssetButPlayerHasNotLoadedYet) { return; } - if ($isViewing) { + if (assetManager.showAssetViewer) { handlePromiseError(handleAction('initPlayer[AssetViewOpen]', 'pause')); } else { handlePromiseError(handleAction('initPlayer[AssetViewClosed]', 'reset')); @@ -296,7 +301,7 @@ handleNextAsset() }, @@ -640,6 +645,7 @@ bind:this={memoryGallery} > { - const scrollTarget = $gridScrollTarget?.at; + const scrollTarget = assetManager.gridScrollTarget?.at; let scrolled = false; if (scrollTarget) { scrolled = await scrollToAssetId(scrollTarget); @@ -212,9 +213,9 @@ setTimeout(() => { const asset = $page.url.searchParams.get('at'); if (asset) { - $gridScrollTarget = { at: asset }; + assetManager.gridScrollTarget = { at: asset }; void navigate( - { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget }, + { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: assetManager.gridScrollTarget }, { replaceState: true, forceNavigate: true }, ); } else { @@ -438,12 +439,11 @@ }; const handlePrevious = async () => { - const laterAsset = await timelineManager.getLaterAsset($viewingAsset); + const laterAsset = await timelineManager.getLaterAsset(asset); if (laterAsset) { - const preloadAsset = await timelineManager.getLaterAsset(laterAsset); - const asset = await getAssetInfo({ id: laterAsset.id, key: authManager.key }); - assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []); + // TODO: If preloadAsset is undefined, throw an exception. + // assetManager.preloadAssets = [await timelineManager.getLaterAsset(laterAsset)]; await navigate({ targetRoute: 'current', assetId: laterAsset.id }); } @@ -451,11 +451,10 @@ }; const handleNext = async () => { - const earlierAsset = await timelineManager.getEarlierAsset($viewingAsset); + const earlierAsset = await timelineManager.getEarlierAsset(asset); + if (earlierAsset) { - const preloadAsset = await timelineManager.getEarlierAsset(earlierAsset); - const asset = await getAssetInfo({ id: earlierAsset.id, key: authManager.key }); - assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []); + // assetManager.preloadAssets = [await timelineManager.getEarlierAsset(earlierAsset)]; await navigate({ targetRoute: 'current', assetId: earlierAsset.id }); } @@ -466,18 +465,21 @@ const randomAsset = await timelineManager.getRandomAsset(); if (randomAsset) { - const asset = await getAssetInfo({ id: randomAsset.id, key: authManager.key }); - assetViewingStore.setAsset(asset); await navigate({ targetRoute: 'current', assetId: randomAsset.id }); - return asset; } + + return !!randomAsset; }; const handleClose = async (asset: { id: string }) => { - assetViewingStore.showAssetViewer(false); + assetManager.showAssetViewer = false; showSkeleton = true; - $gridScrollTarget = { at: asset.id }; - await navigate({ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget }); + assetManager.gridScrollTarget = { at: asset.id }; + await navigate({ + targetRoute: 'current', + assetId: null, + assetGridRouteSearchParams: assetManager.gridScrollTarget, + }); }; const handlePreAction = async (action: Action) => { @@ -722,7 +724,7 @@ let shortcutList = $derived( (() => { - if (searchStore.isSearchEnabled || $showAssetViewer) { + if (searchStore.isSearchEnabled || assetManager.showAssetViewer) { return []; } @@ -775,8 +777,8 @@ }); $effect(() => { - if ($showAssetViewer) { - const { localDateTime } = getTimes($viewingAsset.fileCreatedAt, DateTime.local().offset / 60); + if (assetManager.showAssetViewer) { + const { localDateTime } = getTimes(asset.fileCreatedAt, DateTime.local().offset / 60); void timelineManager.loadMonthGroup({ year: localDateTime.year, month: localDateTime.month }); } }); @@ -923,12 +925,11 @@

- {#if $showAssetViewer} + {#if assetManager.showAssetViewer} {#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }} { + // TODO: defer init until trigger updateOptions + if (assets.length === 1) { + void assetManager.updateOptions({ assetId: assets[0].id }); + } + });
@@ -142,19 +151,17 @@ {/if}
- +
{:else if assets.length === 1} - {#await getAssetInfo({ id: assets[0].id, key: authManager.key }) then asset} - Promise.resolve(false)} - onNext={() => Promise.resolve(false)} - onRandom={() => Promise.resolve(undefined)} - onClose={() => {}} - /> - {/await} + Promise.resolve(false)} + onNext={() => Promise.resolve(false)} + onRandom={() => Promise.resolve(false)} + onClose={() => {}} + /> {/if}
diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 09998ed060..34e5252b18 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -4,11 +4,11 @@ import type { Action } from '$lib/components/asset-viewer/actions/action'; import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; import { AppRoute, AssetAction } from '$lib/constants'; + import type { AssetManager } from '$lib/managers/asset-manager.svelte'; import { modalManager } from '$lib/managers/modal-manager.svelte'; import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types'; import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; - import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { showDeleteModal } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { handlePromiseError } from '$lib/utils'; @@ -29,6 +29,7 @@ interface Props { assets: (TimelineAsset | AssetResponseDto)[]; assetInteraction: AssetInteraction; + assetManager: AssetManager; disableAssetSelect?: boolean; showArchiveIcon?: boolean; viewport: Viewport; @@ -46,6 +47,7 @@ let { assets = $bindable(), assetInteraction, + assetManager = $bindable(), disableAssetSelect = false, showArchiveIcon = false, viewport, @@ -60,8 +62,6 @@ pageHeaderOffset = 0, }: Props = $props(); - let { isViewing: isViewerOpen, asset: viewingAsset, setAssetId } = assetViewingStore; - let geometry: CommonJustifiedLayout | undefined = $state(); $effect(() => { @@ -151,8 +151,7 @@ }); const viewAssetHandler = async (asset: TimelineAsset) => { currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id); - await setAssetId(assets[currentViewAssetIndex].id); - await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); + await navigate({ targetRoute: 'current', assetId: assets[currentViewAssetIndex].id }); }; const selectAllAssets = () => { @@ -292,7 +291,7 @@ const shortcutList = $derived( (() => { - if ($isViewerOpen) { + if (assetManager.showAssetViewer) { return []; } @@ -344,7 +343,7 @@ } }; - const handleRandom = async (): Promise<{ id: string } | undefined> => { + const handleRandom = async (): Promise => { try { let asset: { id: string } | undefined; if (onRandom) { @@ -357,14 +356,14 @@ } if (!asset) { - return; + return false; } await navigateToAsset(asset); - return asset; + return true; } catch (error) { handleError(error, $t('errors.cannot_navigate_next_asset')); - return; + return false; } }; @@ -395,9 +394,8 @@ }; const navigateToAsset = async (asset?: { id: string }) => { - if (asset && asset.id !== $viewingAsset.id) { - await setAssetId(asset.id); - await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); + if (asset && asset.id !== assetManager.asset.id) { + await navigate({ targetRoute: 'current', assetId: asset.id }); } }; @@ -415,7 +413,7 @@ } else if (currentViewAssetIndex === assets.length) { await handlePrevious(); } else { - await setAssetId(assets[currentViewAssetIndex].id); + await navigate({ targetRoute: 'current', assetId: assets[currentViewAssetIndex].id }); } break; } @@ -512,16 +510,16 @@ {/if} -{#if $isViewerOpen} +{#if assetManager.showAssetViewer} { - assetViewingStore.showAssetViewer(false); + assetManager.showAssetViewer = false; handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); }} /> diff --git a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte index bbea0a7a27..fb14c56e09 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte @@ -2,7 +2,7 @@ import { shortcuts } from '$lib/actions/shortcut'; import Portal from '$lib/components/shared-components/portal/portal.svelte'; import DuplicateAsset from '$lib/components/utilities-page/duplicates/duplicate-asset.svelte'; - import { assetViewingStore } from '$lib/stores/asset-viewing.store'; + import type { AssetManager } from '$lib/managers/asset-manager.svelte'; import { handlePromiseError } from '$lib/utils'; import { suggestDuplicate } from '$lib/utils/duplicate-utils'; import { navigate } from '$lib/utils/navigation'; @@ -15,12 +15,12 @@ interface Props { assets: AssetResponseDto[]; + assetManager: AssetManager; onResolve: (duplicateAssetIds: string[], trashIds: string[]) => void; onStack: (assets: AssetResponseDto[]) => void; } - let { assets, onResolve, onStack }: Props = $props(); - const { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore; + let { assets, assetManager, onResolve, onStack }: Props = $props(); const getAssetIndex = (id: string) => assets.findIndex((asset) => asset.id === id); // eslint-disable-next-line svelte/no-unnecessary-state-wrap @@ -39,35 +39,34 @@ }); onDestroy(() => { - assetViewingStore.showAssetViewer(false); + assetManager.showAssetViewer = false; }); - const onNext = () => { - const index = getAssetIndex($viewingAsset.id) + 1; + const onNext = async () => { + const index = getAssetIndex(assetManager.asset.id) + 1; if (index >= assets.length) { - return Promise.resolve(false); + return false; } - setAsset(assets[index]); - return Promise.resolve(true); + await navigate({ targetRoute: 'current', assetId: assets[index].id }); + return true; }; - const onPrevious = () => { - const index = getAssetIndex($viewingAsset.id) - 1; + const onPrevious = async () => { + const index = getAssetIndex(assetManager.asset.id) - 1; if (index < 0) { - return Promise.resolve(false); + return false; } - setAsset(assets[index]); - return Promise.resolve(true); + await navigate({ targetRoute: 'current', assetId: assets[index].id }); + return true; }; - const onRandom = () => { + const onRandom = async () => { if (assets.length <= 0) { - return Promise.resolve(undefined); + return false; } const index = Math.floor(Math.random() * assets.length); - const asset = assets[index]; - setAsset(asset); - return Promise.resolve(asset); + await navigate({ targetRoute: 'current', assetId: assets[index].id }); + return true; }; const onSelectAsset = (asset: AssetResponseDto) => { @@ -102,9 +101,7 @@ { shortcut: { key: 'a' }, onShortcut: onSelectAll }, { shortcut: { key: 's' }, - onShortcut: () => { - setAsset(assets[0]); - }, + onShortcut: () => navigate({ targetRoute: 'current', assetId: assets[0].id }), }, { shortcut: { key: 'd' }, onShortcut: onSelectNone }, { shortcut: { key: 'c', shift: true }, onShortcut: handleResolve }, @@ -170,23 +167,23 @@ {asset} {onSelectAsset} isSelected={selectedAssetIds.has(asset.id)} - onViewAsset={(asset) => setAsset(asset)} + onViewAsset={(asset) => navigate({ targetRoute: 'current', assetId: asset.id })} /> {/each} -{#if $showAssetViewer} +{#if assetManager.showAssetViewer} {#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }} 1} {onNext} {onPrevious} {onRandom} onClose={() => { - assetViewingStore.showAssetViewer(false); + assetManager.showAssetViewer = false; handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); }} /> diff --git a/web/src/lib/managers/asset-manager.svelte.ts b/web/src/lib/managers/asset-manager.svelte.ts new file mode 100644 index 0000000000..b294a02d1a --- /dev/null +++ b/web/src/lib/managers/asset-manager.svelte.ts @@ -0,0 +1,143 @@ +import { authManager } from '$lib/managers/auth-manager.svelte'; +import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; +import { CancellableTask } from '$lib/utils/cancellable-task'; +import type { AssetGridRouteSearchParams } from '$lib/utils/navigation'; +import { + AssetMediaSize, + AssetTypeEnum, + AssetVisibility, + getAllAlbums, + getAssetInfo, + getAssetOriginalPath, + getAssetPlaybackPath, + getAssetThumbnailPath, + getBaseUrl, + type AlbumResponseDto, + type AssetResponseDto, +} from '@immich/sdk'; +import { isEqual } from 'lodash-es'; + +export type AssetManagerOptions = { + assetId?: string; + preloadAssetIds?: string[]; + size?: AssetMediaSize; +}; + +export class AssetManager { + isInitialized = $state(false); + #asset: AssetResponseDto | undefined = $state(); + preloadAssets: TimelineAsset[] = $state([]); + cacheKey: string | null = $state(null); + albums: AlbumResponseDto[] = $state([]); + + showAssetViewer: boolean = $state(false); + gridScrollTarget: AssetGridRouteSearchParams | undefined = $state(); + + initTask = new CancellableTask( + () => (this.isInitialized = true), + () => (this.isInitialized = false), + () => void 0, + ); + + // TODO: Delete this after development + #emptyAsset: AssetResponseDto = { + checksum: '', + deviceAssetId: '', + deviceId: '', + duration: '', + fileCreatedAt: '', + fileModifiedAt: '', + hasMetadata: true, + id: '', + isArchived: false, + isFavorite: false, + isOffline: false, + isTrashed: false, + localDateTime: '', + originalFileName: '', + originalPath: '', + ownerId: '', + thumbhash: null, + type: AssetTypeEnum.Image, + updatedAt: '', + visibility: AssetVisibility.Timeline, + }; + + static #INIT_OPTIONS = {}; + #options: AssetManagerOptions = AssetManager.#INIT_OPTIONS; + + constructor() {} + + async #initializeAsset(id: string) { + const assetResponse = await getAssetInfo({ id, key: authManager.key }); + + if (!assetResponse) { + return; + } + this.#asset = assetResponse; + + const albumsResponse = await getAllAlbums({ assetId: this.asset.id }); + + if (!albumsResponse) { + return; + } + this.albums = albumsResponse; + } + + async refreshAlbums() {} + + async refreshAsset() {} + + async updateOptions(options: AssetManagerOptions) { + if (this.#options !== AssetManager.#INIT_OPTIONS && isEqual(this.#options, options)) { + return; + } + await this.initTask.reset(); + await this.#init(options); + } + + async #init(options: AssetManagerOptions) { + this.isInitialized = false; + await this.initTask.execute(async () => { + this.#options = options; + // TODO: If assetId is undefined, throw an exception. + await this.#initializeAsset(this.#options.assetId!); + // TODO: Preload assets. + }, true); + } + + public destroy() { + this.isInitialized = false; + } + + #createUrl(path: string, parameters?: Record) { + const searchParameters = new URLSearchParams(); + for (const key in parameters) { + const value = parameters[key]; + if (value !== undefined && value !== null) { + searchParameters.set(key, value.toString()); + } + } + return getBaseUrl() + path + searchParameters.toString(); + } + + get asset() { + return this.#asset ?? this.#emptyAsset; + } + + get originalUrl() { + return this.#createUrl(getAssetOriginalPath(this.asset.id), { key: authManager.key, c: this.cacheKey }); + } + + get thumbnailUrl() { + return this.#createUrl(getAssetThumbnailPath(this.asset.id), { + key: authManager.key, + c: this.cacheKey, + size: this.#options.size, + }); + } + + get playbackUrl() { + return this.#createUrl(getAssetPlaybackPath(this.asset.id), { key: authManager.key, c: this.cacheKey }); + } +} diff --git a/web/src/lib/stores/asset-viewing.store.ts b/web/src/lib/stores/asset-viewing.store.ts deleted file mode 100644 index 2cd20d9d20..0000000000 --- a/web/src/lib/stores/asset-viewing.store.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { authManager } from '$lib/managers/auth-manager.svelte'; -import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; -import { type AssetGridRouteSearchParams } from '$lib/utils/navigation'; -import { getAssetInfo, type AssetResponseDto } from '@immich/sdk'; -import { readonly, writable } from 'svelte/store'; - -function createAssetViewingStore() { - const viewingAssetStoreState = writable(); - const preloadAssets = writable([]); - const viewState = writable(false); - const gridScrollTarget = writable(); - - const setAsset = (asset: AssetResponseDto, assetsToPreload: TimelineAsset[] = []) => { - preloadAssets.set(assetsToPreload); - viewingAssetStoreState.set(asset); - viewState.set(true); - }; - - const setAssetId = async (id: string): Promise => { - const asset = await getAssetInfo({ id, key: authManager.key }); - setAsset(asset); - return asset; - }; - - const showAssetViewer = (show: boolean) => { - viewState.set(show); - }; - - return { - asset: readonly(viewingAssetStoreState), - preloadAssets: readonly(preloadAssets), - isViewing: viewState, - gridScrollTarget, - setAsset, - setAssetId, - showAssetViewer, - }; -} - -export const assetViewingStore = createAssetViewingStore(); diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts index 2e5a353cf8..b7995d753b 100644 --- a/web/src/lib/utils/navigation.ts +++ b/web/src/lib/utils/navigation.ts @@ -1,7 +1,6 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { AppRoute } from '$lib/constants'; -import { getAssetInfo } from '@immich/sdk'; import type { NavigationTarget } from '@sveltejs/kit'; import { get } from 'svelte/store'; @@ -22,10 +21,6 @@ export const isLockedFolderRoute = (route?: string | null) => !!route?.startsWit export const isAssetViewerRoute = (target?: NavigationTarget | null) => !!(target?.route.id?.endsWith('/[[assetId=id]]') && 'assetId' in (target?.params || {})); -export function getAssetInfoFromParam({ assetId, key }: { assetId?: string; key?: string }) { - return assetId ? getAssetInfo({ id: assetId, key }) : undefined; -} - function currentUrlWithoutAsset() { const $page = get(page); // This contains special casing for the /photos/:assetId route, which hangs directly diff --git a/web/src/lib/utils/slideshow-history.ts b/web/src/lib/utils/slideshow-history.ts index 2452a3a147..b06323e320 100644 --- a/web/src/lib/utils/slideshow-history.ts +++ b/web/src/lib/utils/slideshow-history.ts @@ -2,7 +2,7 @@ export class SlideshowHistory { private history: { id: string }[] = []; private index = 0; - constructor(private onChange: (asset: { id: string }) => void) {} + constructor(private onChange: (asset: { id: string }) => Promise) {} reset() { this.history = []; @@ -18,23 +18,23 @@ export class SlideshowHistory { } } - next(): boolean { + async next(): Promise { if (this.index === this.history.length - 1) { return false; } this.index++; - this.onChange(this.history[this.index]); + await this.onChange(this.history[this.index]); return true; } - previous(): boolean { + async previous(): Promise { if (this.index === 0) { return false; } this.index--; - this.onChange(this.history[this.index]); + await this.onChange(this.history[this.index]); return true; } } diff --git a/web/src/routes/(user)/+layout.svelte b/web/src/routes/(user)/+layout.svelte index ea10c45444..2c011b9b6d 100644 --- a/web/src/routes/(user)/+layout.svelte +++ b/web/src/routes/(user)/+layout.svelte @@ -1,32 +1,23 @@ -
+ +
{@render children?.()}
diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index aa535a30ee..fe9d3bd718 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -34,6 +34,7 @@ import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import { AlbumPageViewMode, AppRoute } from '$lib/constants'; import { activityManager } from '$lib/managers/activity-manager.svelte'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { modalManager } from '$lib/managers/modal-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; @@ -43,7 +44,6 @@ import QrCodeModal from '$lib/modals/QrCodeModal.svelte'; import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; - import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { preferences, user } from '$lib/stores/user.store'; @@ -95,7 +95,6 @@ let { data = $bindable() }: Props = $props(); - let { isViewing: showAssetViewer, setAssetId, gridScrollTarget } = assetViewingStore; let { slideshowState, slideshowNavigation } = slideshowStore; let oldAt: AssetGridRouteSearchParams | null | undefined = $state(); @@ -109,6 +108,15 @@ const assetInteraction = new AssetInteraction(); const timelineInteraction = new AssetInteraction(); + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + afterNavigate(({ from }) => { let url: string | undefined = from?.url?.pathname; @@ -148,7 +156,8 @@ ? await timelineManager.getRandomAsset() : timelineManager.months[0]?.dayGroups[0]?.viewerAssets[0]?.asset; if (asset) { - handlePromiseError(setAssetId(asset.id).then(() => ($slideshowState = SlideshowState.PlaySlideshow))); + await navigate({ targetRoute: 'current', assetId: asset.id }); + $slideshowState = SlideshowState.PlaySlideshow; } }; @@ -166,7 +175,7 @@ viewMode = AlbumPageViewMode.VIEW; return; } - if ($showAssetViewer) { + if (assetManager.showAssetViewer) { return; } if (assetInteraction.selectionActive) { @@ -346,7 +355,7 @@ const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 0); $effect(() => { - if ($showAssetViewer || !isShared) { + if (assetManager.showAssetViewer || !isShared) { return; } @@ -361,7 +370,9 @@ let isOwned = $derived($user.id == album.ownerId); let showActivityStatus = $derived( - album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || activityManager.commentCount > 0), + album.albumUsers.length > 0 && + !assetManager.showAssetViewer && + (album.isActivityEnabled || activityManager.commentCount > 0), ); let isEditor = $derived( album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor || @@ -449,6 +460,7 @@ {album} {timelineManager} assetInteraction={currentAssetIntersection} + {assetManager} {isShared} {isSelectionMode} {singleSelect} @@ -626,7 +638,7 @@ onclick={async () => { timelineManager.suspendTransitions = true; viewMode = AlbumPageViewMode.SELECT_ASSETS; - oldAt = { at: $gridScrollTarget?.at }; + oldAt = { at: assetManager.gridScrollTarget?.at }; await navigate( { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: null } }, { replaceState: true }, @@ -648,7 +660,7 @@ {/if} {#if $featureFlags.loaded && $featureFlags.map} - + {/if} {#if album.assetCount > 0} @@ -735,7 +747,7 @@ {/if} {/if}
- {#if album.albumUsers.length > 0 && album && isShowActivity && $user && !$showAssetViewer} + {#if album.albumUsers.length > 0 && album && isShowActivity && $user && !assetManager.showAssetViewer}
{ await authenticate(url); - const [album, asset] = await Promise.all([ - getAlbumInfo({ id: params.albumId, withoutAssets: true }), - getAssetInfoFromParam(params), - ]); + const album = await getAlbumInfo({ id: params.albumId, withoutAssets: true }); return { album, - asset, + assetId: params.assetId, meta: { title: album.albumName, }, diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5481224057..8d9a2fca3f 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -14,6 +14,7 @@ import { AssetAction } from '$lib/constants'; import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetVisibility } from '@immich/sdk'; @@ -33,6 +34,15 @@ const assetInteraction = new AssetInteraction(); + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + const handleEscape = () => { if (assetInteraction.selectionActive) { assetInteraction.clearMultiselect(); @@ -51,6 +61,7 @@ enableRouting={true} {timelineManager} {assetInteraction} + {assetManager} removeAction={AssetAction.UNARCHIVE} onEscape={handleEscape} > diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.ts index f5d4560505..8a0a1d323f 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,15 +1,13 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { await authenticate(url); - const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); return { - asset, + assetId: params.assetId, meta: { title: $t('archive'), }, diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index ae24a82da7..3684fa5599 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -17,6 +17,7 @@ import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { AssetAction } from '$lib/constants'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { preferences } from '$lib/stores/user.store'; @@ -37,6 +38,15 @@ const assetInteraction = new AssetInteraction(); + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + const handleEscape = () => { if (assetInteraction.selectionActive) { assetInteraction.clearMultiselect(); @@ -56,6 +66,7 @@ withStacked={true} {timelineManager} {assetInteraction} + {assetManager} removeAction={AssetAction.UNFAVORITE} onEscape={handleEscape} > diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.ts index 0d9fe7a203..d9a606eb22 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,15 +1,13 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { await authenticate(url); - const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); return { - asset, + assetId: params.assetId, meta: { title: $t('favorites'), }, diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index 84ae328ccc..3fdd7268b4 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -21,8 +21,8 @@ import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; import Sidebar from '$lib/components/sidebar/sidebar.svelte'; import { AppRoute, QueryParameter } from '$lib/constants'; - import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import type { Viewport } from '$lib/managers/timeline-manager/types'; + import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { foldersStore } from '$lib/stores/folders.svelte'; import { preferences } from '$lib/stores/user.store'; import { cancelMultiselect } from '$lib/utils/asset-utils'; @@ -32,6 +32,8 @@ import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; + import { onDestroy } from 'svelte'; interface Props { data: PageData; @@ -43,6 +45,15 @@ const assetInteraction = new AssetInteraction(); + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + const handleNavigateToFolder = (folderName: string) => navigateToView(joinPaths(data.tree.path, folderName)); function getLinkForPath(path: string) { @@ -106,6 +117,7 @@ { await authenticate(url); - const [, asset, $t] = await Promise.all([foldersStore.fetchTree(), getAssetInfoFromParam(params), getFormatter()]); + const [, $t] = await Promise.all([foldersStore.fetchTree(), getFormatter()]); let tree = foldersStore.folders!; const path = url.searchParams.get(QueryParameter.PATH); @@ -23,7 +22,7 @@ export const load = (async ({ params, url }) => { const pathAssets = tree.hasAssets ? await foldersStore.fetchAssetsByPath(tree.path) : null; return { - asset, + assetId: params.assetId, tree, pathAssets, meta: { diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte index 733e93db71..8a4346bf8d 100644 --- a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -12,6 +12,7 @@ import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { AppRoute, AssetAction } from '$lib/constants'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetVisibility, lockAuthSession } from '@immich/sdk'; @@ -33,6 +34,15 @@ const assetInteraction = new AssetInteraction(); + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + const handleEscape = () => { if (assetInteraction.selectionActive) { assetInteraction.clearMultiselect(); @@ -62,6 +72,7 @@ enableRouting={true} {timelineManager} {assetInteraction} + {assetManager} onEscape={handleEscape} removeAction={AssetAction.SET_VISIBILITY_TIMELINE} > diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts index cf2e415da0..55c19acce9 100644 --- a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,7 +1,6 @@ import { AppRoute } from '$lib/constants'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import { getAuthStatus } from '@immich/sdk'; import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; @@ -14,11 +13,10 @@ export const load = (async ({ params, url }) => { redirect(302, `${AppRoute.AUTH_PIN_PROMPT}?continue=${encodeURIComponent(url.pathname + url.search)}`); } - const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); return { - asset, + assetId: params.assetId, meta: { title: $t('locked_folder'), }, diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte index b1fff3a0cd..24ddd09665 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,12 +1,10 @@ @@ -76,16 +78,16 @@
- {#if $showAssetViewer} + {#if assetManager.showAssetViewer} {#await import('../../../../../lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }} 1} onNext={navigateNext} onPrevious={navigatePrevious} onRandom={navigateRandom} onClose={() => { - assetViewingStore.showAssetViewer(false); + assetManager.showAssetViewer = false; handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); }} isShared={false} diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.ts index add9882bcd..e46d4d3eb3 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,15 +1,12 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; - export const load = (async ({ params, url }) => { await authenticate(url); - const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); return { - asset, + assetId: params.assetId, meta: { title: $t('map'), }, diff --git a/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.svelte index 7f34913224..1503bfa7fe 100644 --- a/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,5 +1,23 @@ - - + diff --git a/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.ts index 5c030da72f..333262384f 100644 --- a/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,16 +1,14 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { const user = await authenticate(url); - const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); return { user, - asset, + assetId: params.assetId, meta: { title: $t('memory'), }, diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 83962c5a90..a65aab686e 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -8,6 +8,7 @@ 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 { AppRoute } from '$lib/constants'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetVisibility } from '@immich/sdk'; @@ -34,6 +35,15 @@ onDestroy(() => timelineManager.destroy()); const assetInteraction = new AssetInteraction(); + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + const handleEscape = () => { if (assetInteraction.selectionActive) { assetInteraction.clearMultiselect(); @@ -43,7 +53,7 @@
- +
{#if assetInteraction.selectionActive} diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.ts index 1977d9a095..fb87856805 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,6 +1,5 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import { getUser } from '@immich/sdk'; import type { PageLoad } from './$types'; @@ -8,11 +7,10 @@ export const load = (async ({ params, url }) => { await authenticate(url); const partner = await getUser({ id: params.userId }); - const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); return { - asset, + assetId: params.assetId, partner, meta: { title: $t('partner'), diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 70fe6a41d2..7c9fbf86be 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -31,13 +31,13 @@ notificationController, } from '$lib/components/shared-components/notification/notification'; import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { modalManager } from '$lib/managers/modal-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import PersonEditBirthDateModal from '$lib/modals/PersonEditBirthDateModal.svelte'; import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; - import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { locale } from '$lib/stores/preferences.store'; import { preferences } from '$lib/stores/user.store'; import { websocketEvents } from '$lib/stores/websocket'; @@ -75,7 +75,6 @@ let { data }: Props = $props(); let numberOfAssets = $state(data.statistics.assets); - let { isViewing: showAssetViewer } = assetViewingStore; const timelineManager = new TimelineManager(); $effect(() => void timelineManager.updateOptions({ visibility: AssetVisibility.Timeline, personId: data.person.id })); @@ -83,6 +82,15 @@ const assetInteraction = new AssetInteraction(); + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS); let isEditingName = $state(false); let previousRoute: string = $state(AppRoute.EXPLORE); @@ -123,7 +131,7 @@ }); const handleEscape = async () => { - if ($showAssetViewer) { + if (assetManager.showAssetViewer) { return; } if (assetInteraction.selectionActive) { @@ -388,6 +396,7 @@ {person} {timelineManager} {assetInteraction} + {assetManager} isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON} singleSelect={viewMode === PersonPageViewMode.SELECT_PERSON} onSelect={handleSelectFeaturePhoto} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.ts index 92371bd34e..55b2d7b7a6 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,23 +1,21 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import { getPerson, getPersonStatistics } from '@immich/sdk'; import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { await authenticate(url); - const [person, statistics, asset] = await Promise.all([ + const [person, statistics] = await Promise.all([ getPerson({ id: params.personId }), getPersonStatistics({ id: params.personId }), - getAssetInfoFromParam(params), ]); const $t = await getFormatter(); return { person, statistics, - asset, + assetId: params.assetId, meta: { title: person.name || $t('person'), }, diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 0098667873..189d3558e2 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -22,9 +22,9 @@ import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { AssetAction } from '$lib/constants'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; - import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { preferences, user } from '$lib/stores/user.store'; import { @@ -39,12 +39,27 @@ import { mdiDotsVertical, mdiPlus } from '@mdi/js'; import { onDestroy } from 'svelte'; import { t } from 'svelte-i18n'; + import type { PageData } from './$types'; + + interface Props { + data: PageData; + } + + let { data }: Props = $props(); - let { isViewing: showAssetViewer } = assetViewingStore; const timelineManager = new TimelineManager(); void timelineManager.updateOptions({ visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true }); onDestroy(() => timelineManager.destroy()); + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + const assetInteraction = new AssetInteraction(); let selectedAssets = $derived(assetInteraction.selectedAssets); @@ -59,7 +74,7 @@ return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate); }); const handleEscape = () => { - if ($showAssetViewer) { + if (assetManager.showAssetViewer) { return; } if (assetInteraction.selectionActive) { @@ -93,6 +108,7 @@ enableRouting={true} {timelineManager} {assetInteraction} + {assetManager} removeAction={AssetAction.ARCHIVE} onEscape={handleEscape} withStacked diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.ts b/web/src/routes/(user)/photos/[[assetId=id]]/+page.ts index 209b5483a8..b296386ceb 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.ts @@ -1,15 +1,13 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { await authenticate(url); - const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); return { - asset, + assetId: params.assetId, meta: { title: $t('photos'), }, diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index 092eb3b0d4..0ea0a383f5 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -23,10 +23,10 @@ import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; import { AppRoute, QueryParameter } from '$lib/constants'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; - import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { lang, locale } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { preferences } from '$lib/stores/user.store'; @@ -47,11 +47,17 @@ } from '@immich/sdk'; import { IconButton } from '@immich/ui'; import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; - import { tick } from 'svelte'; + import { onDestroy, tick } from 'svelte'; import { t } from 'svelte-i18n'; + import type { PageData } from './$types'; + + interface Props { + data: PageData; + } + + let { data }: Props = $props(); const MAX_ASSET_COUNT = 5000; - let { isViewing: showAssetViewer } = assetViewingStore; const viewport: Viewport = $state({ width: 0, height: 0 }); // The GalleryViewer pushes it's own history state, which causes weird @@ -83,8 +89,17 @@ let timelineManager = new TimelineManager(); + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + const onEscape = () => { - if ($showAssetViewer) { + if (assetManager.showAssetViewer) { return; } @@ -379,6 +394,7 @@ { await authenticate(url); - const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); return { - asset, + assetId: params.assetId, meta: { title: $t('search'), }, diff --git a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte index d16ba622e9..86b61dfbd1 100644 --- a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -5,14 +5,14 @@ import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte'; import PasswordField from '$lib/components/shared-components/password-field.svelte'; import ThemeButton from '$lib/components/shared-components/theme-button.svelte'; - import { assetViewingStore } from '$lib/stores/asset-viewing.store'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { user } from '$lib/stores/user.store'; import { setSharedLink } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; import { navigate } from '$lib/utils/navigation'; import { getMySharedLink, SharedLinkType } from '@immich/sdk'; import { Button } from '@immich/ui'; - import { tick } from 'svelte'; + import { onDestroy, tick } from 'svelte'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -22,12 +22,20 @@ let { data }: Props = $props(); - let { gridScrollTarget } = assetViewingStore; let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = $state(data); let { title, description } = $state(meta); let isOwned = $derived($user ? $user.id === sharedLink?.userId : false); let password = $state(''); + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + const handlePasswordSubmit = async () => { try { sharedLink = await getMySharedLink({ password, key }); @@ -39,7 +47,7 @@ $t('shared_photos_and_videos_count', { values: { assetCount: sharedLink.assets.length } }); await tick(); await navigate( - { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget }, + { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: assetManager.gridScrollTarget }, { forceNavigate: true, replaceState: true }, ); } catch (error) { @@ -88,10 +96,10 @@ {/if} {#if !passwordRequired && sharedLink?.type == SharedLinkType.Album} - + {/if} {#if !passwordRequired && sharedLink?.type == SharedLinkType.Individual}
- +
{/if} diff --git a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts index c0edb5e669..164b433312 100644 --- a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,7 +1,6 @@ import { getAssetThumbnailUrl, setSharedLink } from '$lib/utils'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import { getMySharedLink, isHttpError } from '@immich/sdk'; import type { PageLoad } from './$types'; @@ -12,7 +11,7 @@ export const load = (async ({ params, url }) => { const $t = await getFormatter(); try { - const [sharedLink, asset] = await Promise.all([getMySharedLink({ key }), getAssetInfoFromParam(params)]); + const sharedLink = await getMySharedLink({ key }); setSharedLink(sharedLink); const assetCount = sharedLink.assets.length; const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id; @@ -21,7 +20,7 @@ export const load = (async ({ params, url }) => { return { sharedLink, sharedLinkKey: key, - asset, + assetId: params.assetId, meta: { title: sharedLink.album ? sharedLink.album.albumName : $t('public_share'), description: sharedLink.description || $t('shared_photos_and_videos_count', { values: { assetCount } }), diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte index 503ea72d54..e3ea9bf815 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -20,6 +20,7 @@ import { onDestroy } from 'svelte'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; interface Props { data: PageData; @@ -33,6 +34,15 @@ $effect(() => void timelineManager.updateOptions({ deferInit: !tag, tagId: tag?.id })); onDestroy(() => timelineManager.destroy()); + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + let tags = $derived(data.tags); const tree = $derived(TreeNode.fromTags(tags)); const tag = $derived(tree.traverse(data.path)); @@ -118,7 +128,13 @@
{#if tag.hasAssets} - + {#snippet empty()} {/snippet} diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts index 225d79a38d..3dbf143af0 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,13 +1,11 @@ import { QueryParameter } from '$lib/constants'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import { getAllTags } from '@immich/sdk'; import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { await authenticate(url); - const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); const tags = await getAllTags(); @@ -15,7 +13,7 @@ export const load = (async ({ params, url }) => { return { path: url.searchParams.get(QueryParameter.PATH) ?? '', tags, - asset, + assetId: params.assetId, meta: { title: $t('tags'), }, diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index bae1786891..0350929447 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -13,6 +13,7 @@ notificationController, } from '$lib/components/shared-components/notification/notification'; import { AppRoute } from '$lib/constants'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { modalManager } from '$lib/managers/modal-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; @@ -42,6 +43,15 @@ const assetInteraction = new AssetInteraction(); + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + const handleEmptyTrash = async () => { const isConfirmed = await modalManager.showDialog({ prompt: $t('empty_trash_confirmation') }); if (!isConfirmed) { @@ -117,7 +127,7 @@ {/snippet} - +

{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}

diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.ts index 79c41892c7..cce96430d6 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,15 +1,13 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { await authenticate(url); - const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); return { - asset, + assetId: params.assetId, meta: { title: $t('trash'), }, diff --git a/web/src/routes/(user)/utilities/+page.ts b/web/src/routes/(user)/utilities/+page.ts index af241d0fd7..dbb939495b 100644 --- a/web/src/routes/(user)/utilities/+page.ts +++ b/web/src/routes/(user)/utilities/+page.ts @@ -1,15 +1,12 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import type { PageLoad } from './$types'; -export const load = (async ({ params, url }) => { +export const load = (async ({ url }) => { await authenticate(url); - const asset = await getAssetInfoFromParam(params); const $t = await getFormatter(); return { - asset, meta: { title: $t('utilities'), }, diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte index f15a20f6d3..3c4b09a351 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -19,6 +19,8 @@ import { mdiCheckOutline, mdiInformationOutline, mdiKeyboard, mdiTrashCanOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; + import { onDestroy } from 'svelte'; + import { AssetManager } from '$lib/managers/asset-manager.svelte'; interface Props { data: PageData; @@ -36,6 +38,15 @@ info?: string; } + const assetManager = new AssetManager(); + $effect(() => { + if (data.assetId) { + assetManager.showAssetViewer = true; + void assetManager.updateOptions({ assetId: data.assetId }); + } + }); + onDestroy(() => assetManager.destroy()); + const duplicateShortcuts: Shortcuts = { general: [], actions: [ @@ -207,6 +218,7 @@ {#key duplicates[0].duplicateId} handleResolve(duplicates[0].duplicateId, duplicateAssetIds, trashIds)} onStack={(assets) => handleStack(duplicates[0].duplicateId, assets)} diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.ts index 978f50830e..9e1ed3203a 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,17 +1,15 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getAssetInfoFromParam } from '$lib/utils/navigation'; import { getAssetDuplicates } from '@immich/sdk'; import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { await authenticate(url); - const asset = await getAssetInfoFromParam(params); const duplicates = await getAssetDuplicates(); const $t = await getFormatter(); return { - asset, + assetId: params.assetId, duplicates, meta: { title: $t('duplicates'),