From 8b41195d221a8f82d91d0badf5ffe68015b86a7f Mon Sep 17 00:00:00 2001 From: midzelis Date: Sat, 7 Mar 2026 21:14:45 +0000 Subject: [PATCH] fix(web): preserve stacked asset selection when tagging faces Change-Id: Iec1507560f99f2e9433bd5cf6b460b176a6a6964 --- .../asset-viewer/AssetViewer.svelte | 78 ++++++++++++------- .../asset-viewer/DetailPanel.svelte | 18 +---- .../asset-viewer/PhotoViewer.svelte | 11 ++- .../face-editor/FaceEditor.svelte | 5 +- .../faces-page/PersonSidePanel.svelte | 5 +- 5 files changed, 70 insertions(+), 47 deletions(-) diff --git a/web/src/lib/components/asset-viewer/AssetViewer.svelte b/web/src/lib/components/asset-viewer/AssetViewer.svelte index 16f7c6cfb0..cb950dcf1b 100644 --- a/web/src/lib/components/asset-viewer/AssetViewer.svelte +++ b/web/src/lib/components/asset-viewer/AssetViewer.svelte @@ -10,6 +10,7 @@ import OnEvents from '$lib/components/OnEvents.svelte'; import { AssetAction, ProjectionType } from '$lib/constants'; import { activityManager } from '$lib/managers/activity-manager.svelte'; + import { assetCacheManager } from '$lib/managers/AssetCacheManager.svelte'; import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { editManager, EditToolType } from '$lib/managers/edit/edit-manager.svelte'; @@ -102,9 +103,10 @@ const stackSelectedThumbnailSize = 65; let previewStackedAsset: AssetResponseDto | undefined = $state(); - let stack: StackResponseDto | null = $state(null); + let stack: StackResponseDto | undefined = $state(); + let selectedStackAsset: AssetResponseDto | undefined = $state(); - const asset = $derived(previewStackedAsset ?? cursor.current); + const asset = $derived(previewStackedAsset ?? selectedStackAsset ?? cursor.current); const nextAsset = $derived(cursor.nextAsset); const previousAsset = $derived(cursor.previousAsset); let sharedLink = getSharedLink(); @@ -117,17 +119,29 @@ playOriginalVideo = value; }; + const selectStackedAsset = async (id: string) => { + ocrManager.clear(); + selectedStackAsset = await assetCacheManager.getAsset({ id }); + if (!sharedLink) { + await ocrManager.getAssetOcr(id); + } + }; + const refreshStack = async () => { if (authManager.isSharedLink || !withStacked) { return; } - if (asset.stack) { - stack = await getStack({ id: asset.stack.id }); + if (!cursor.current.stack) { + stack = undefined; + selectedStackAsset = undefined; + return; } - if (!stack?.assets.some(({ id }) => id === asset.id)) { - stack = null; + stack = await getStack({ id: cursor.current.stack.id }); + const primaryAsset = stack?.assets.find(({ id }) => id === stack?.primaryAssetId); + if (primaryAsset) { + await selectStackedAsset(primaryAsset.id); } }; @@ -185,11 +199,21 @@ onClose?.(asset.id); }; + const refreshPreservingSelection = async () => { + const id = asset.id; + assetCacheManager.invalidateAsset(id); + if (selectedStackAsset) { + await selectStackedAsset(id); + } else { + const refreshedAsset = await assetCacheManager.getAsset({ id }); + assetViewerManager.setAsset(refreshedAsset); + } + onAssetChange?.(asset); + }; + const closeEditor = async () => { if (editManager.hasAppliedEdits) { - const refreshedAsset = await getAssetInfo({ id: asset.id }); - onAssetChange?.(refreshedAsset); - assetViewerManager.setAsset(refreshedAsset); + await refreshPreservingSelection(); } assetViewerManager.closeEditor(); }; @@ -304,10 +328,6 @@ } }; - const handleStackedAssetMouseEvent = (isMouseOver: boolean, stackedAsset: AssetResponseDto) => { - previewStackedAsset = isMouseOver ? stackedAsset : undefined; - }; - const handlePreAction = (action: Action) => { preAction?.(action); }; @@ -320,7 +340,7 @@ break; } case AssetAction.REMOVE_ASSET_FROM_STACK: { - stack = action.stack; + stack = action.stack ?? undefined; if (stack) { cursor.current = stack.assets[0]; } @@ -328,7 +348,7 @@ } case AssetAction.STACK: case AssetAction.SET_STACK_PRIMARY_ASSET: { - stack = action.stack; + stack = action.stack ?? undefined; break; } case AssetAction.SET_PERSON_FEATURED_PHOTO: { @@ -391,7 +411,7 @@ $effect(() => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions - asset; + cursor.current; untrack(() => handlePromiseError(refresh())); }); @@ -560,7 +580,12 @@ {:else if viewerKind === 'CropArea'} {:else if viewerKind === 'PhotoViewer'} - + {:else if viewerKind === 'VideoViewer'} {#if showDetailPanel} - + {:else if assetViewerManager.isShowEditor} {/if} @@ -629,27 +654,24 @@
{#each stackedAssets as stackedAsset (stackedAsset.id)} + {@const isSelected = stackedAsset.id === (selectedStackAsset?.id ?? cursor.current.id)}
{ - cursor.current = stackedAsset; - previewStackedAsset = undefined; - }} - onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)} + onClick={() => selectStackedAsset(stackedAsset.id)} readonly - thumbnailSize={stackedAsset.id === asset.id ? stackSelectedThumbnailSize : stackThumbnailSize} + thumbnailSize={isSelected ? stackSelectedThumbnailSize : stackThumbnailSize} showStackedIcon={false} disableLinkMouseOver /> - {#if stackedAsset.id === asset.id} + {#if isSelected}
diff --git a/web/src/lib/components/asset-viewer/DetailPanel.svelte b/web/src/lib/components/asset-viewer/DetailPanel.svelte index aa32f825a0..d57d1bc00d 100644 --- a/web/src/lib/components/asset-viewer/DetailPanel.svelte +++ b/web/src/lib/components/asset-viewer/DetailPanel.svelte @@ -16,13 +16,7 @@ import { getByteUnitString } from '$lib/utils/byte-units'; import { handleError } from '$lib/utils/handle-error'; import { getParentPath } from '$lib/utils/tree-utils'; - import { - AssetMediaSize, - getAllAlbums, - getAssetInfo, - type AlbumResponseDto, - type AssetResponseDto, - } from '@immich/sdk'; + import { AssetMediaSize, getAllAlbums, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk'; import { Icon, IconButton, LoadingSpinner, Text } from '@immich/ui'; import { mdiCamera, mdiCameraIris, mdiClose, mdiImageOutline, mdiInformationOutline } from '@mdi/js'; import { onDestroy } from 'svelte'; @@ -37,9 +31,10 @@ interface Props { asset: AssetResponseDto; currentAlbum?: AlbumResponseDto | null; + onRefreshPeople?: () => Promise; } - let { asset, currentAlbum = null }: Props = $props(); + let { asset, currentAlbum = null, onRefreshPeople }: Props = $props(); let isOwner = $derived(authManager.authenticated && authManager.user.id === asset.ownerId); let latlng = $derived( @@ -94,11 +89,6 @@ return undefined; }; - const handleRefreshPeople = async () => { - asset = await getAssetInfo({ id: asset.id }); - assetViewerManager.closeEditFacesPanel(); - }; - const getAssetFolderHref = (asset: AssetResponseDto) => { // Remove the last part of the path to get the parent path return Route.folders({ path: getParentPath(asset.originalPath) }); @@ -385,6 +375,6 @@ assetId={asset.id} assetType={asset.type} onClose={() => assetViewerManager.closeEditFacesPanel()} - onRefresh={handleRefreshPeople} + onRefresh={() => void onRefreshPeople?.()} /> {/if} diff --git a/web/src/lib/components/asset-viewer/PhotoViewer.svelte b/web/src/lib/components/asset-viewer/PhotoViewer.svelte index 37844a5459..c4c40ffaf4 100644 --- a/web/src/lib/components/asset-viewer/PhotoViewer.svelte +++ b/web/src/lib/components/asset-viewer/PhotoViewer.svelte @@ -31,9 +31,10 @@ onReady?: () => void; onError?: () => void; onSwipe?: (event: SwipeCustomEvent) => void; + onTagFace?: () => Promise; }; - let { cursor, element = $bindable(), sharedLink, onReady, onError, onSwipe }: Props = $props(); + let { cursor, element = $bindable(), sharedLink, onReady, onError, onSwipe, onTagFace }: Props = $props(); const { slideshowState, slideshowLook } = slideshowStore; const asset = $derived(cursor.current); @@ -287,6 +288,12 @@ {#if assetViewerManager.isFaceEditMode && assetViewerManager.imgRef} - + {/if}
diff --git a/web/src/lib/components/asset-viewer/face-editor/FaceEditor.svelte b/web/src/lib/components/asset-viewer/face-editor/FaceEditor.svelte index 5ec423a9c4..c918736292 100644 --- a/web/src/lib/components/asset-viewer/face-editor/FaceEditor.svelte +++ b/web/src/lib/components/asset-viewer/face-editor/FaceEditor.svelte @@ -18,9 +18,10 @@ containerWidth: number; containerHeight: number; assetId: string; + onTagFace?: () => Promise; }; - let { htmlElement, containerWidth, containerHeight, assetId }: Props = $props(); + let { htmlElement, containerWidth, containerHeight, assetId, onTagFace }: Props = $props(); let canvasEl: HTMLCanvasElement | undefined = $state(); let canvas: Canvas | undefined = $state(); @@ -325,7 +326,7 @@ }, }); - await assetViewerManager.setAssetId(assetId); + await onTagFace?.(); } catch (error) { handleError(error, 'Error tagging face'); } finally { diff --git a/web/src/lib/components/faces-page/PersonSidePanel.svelte b/web/src/lib/components/faces-page/PersonSidePanel.svelte index eedd6766eb..dcdf8c8e72 100644 --- a/web/src/lib/components/faces-page/PersonSidePanel.svelte +++ b/web/src/lib/components/faces-page/PersonSidePanel.svelte @@ -178,7 +178,10 @@ peopleWithFaces = peopleWithFaces.filter((f) => f.id !== face.id); - await assetViewerManager.setAssetId(assetId); + onRefresh(); + if (peopleWithFaces.length === 0) { + onClose(); + } } catch (error) { handleError(error, $t('error_delete_face')); }