From b8779f9bacd2d16145baae4dfdb8a3d91d28d136 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/asset-viewer.svelte | 80 +++++++++++++------ .../asset-viewer/detail-panel.svelte | 18 +---- .../face-editor/face-editor.svelte | 5 +- .../asset-viewer/photo-viewer.svelte | 11 ++- .../faces-page/person-side-panel.svelte | 5 +- 5 files changed, 75 insertions(+), 44 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index bf13cb4399..b63217f366 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -10,6 +10,7 @@ import { AssetAction, ProjectionType } from '$lib/constants'; import { activityManager } from '$lib/managers/activity-manager.svelte'; import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; + import { assetCacheManager } from '$lib/managers/AssetCacheManager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { editManager, EditToolType } from '$lib/managers/edit/edit-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte'; @@ -98,10 +99,11 @@ const stackThumbnailSize = 60; const stackSelectedThumbnailSize = 65; + let stack: StackResponseDto | undefined = $state(); + let selectedStackAsset: AssetResponseDto | undefined = $state(); let previewStackedAsset: AssetResponseDto | undefined = $state(); - let stack: StackResponseDto | null = $state(null); - 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(); @@ -114,17 +116,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); } }; @@ -182,11 +196,21 @@ onClose?.(asset); }; + const refreshPreservingSelection = async () => { + const id = asset.id; + assetCacheManager.invalidateAsset(id); + if (selectedStackAsset) { + await selectStackedAsset(id); + } else { + const asset = await assetCacheManager.getAsset({ id }); + assetViewerManager.setAsset(asset); + } + onAssetChange?.(asset); + }; + const closeEditor = async () => { if (editManager.hasAppliedEdits) { - const refreshedAsset = await getAssetInfo({ id: asset.id }); - onAssetChange?.(refreshedAsset); - assetViewerManager.setAsset(refreshedAsset); + await refreshPreservingSelection(); } assetViewerManager.closeEditor(); }; @@ -285,10 +309,6 @@ } }; - const handleStackedAssetMouseEvent = (isMouseOver: boolean, stackedAsset: AssetResponseDto) => { - previewStackedAsset = isMouseOver ? stackedAsset : undefined; - }; - const handlePreAction = (action: Action) => { preAction?.(action); }; @@ -301,7 +321,7 @@ break; } case AssetAction.REMOVE_ASSET_FROM_STACK: { - stack = action.stack; + stack = action.stack ?? undefined; if (stack) { cursor.current = stack.assets[0]; } @@ -368,7 +388,7 @@ $effect(() => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions - asset; + cursor.current; untrack(() => handlePromiseError(refresh())); }); @@ -533,7 +553,12 @@ {:else if viewerKind === 'CropArea'} {:else if viewerKind === 'PhotoViewer'} - + {:else if viewerKind === 'VideoViewer'} {#if showDetailPanel} - + {:else if assetViewerManager.isShowEditor} {/if} @@ -606,22 +631,27 @@ brokenAssetClass="text-xs" dimmed={stackedAsset.id !== asset.id} asset={toTimelineAsset(stackedAsset)} - onClick={() => { - cursor.current = stackedAsset; + onClick={async () => { + await selectStackedAsset(stackedAsset.id); previewStackedAsset = undefined; }} - onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)} + onMouseEvent={async ({ isMouseOver }) => { + if (isMouseOver) { + previewStackedAsset = stackedAsset; + previewStackedAsset = await assetCacheManager.getAsset({ id: stackedAsset.id }); + } else { + previewStackedAsset = undefined; + } + }} readonly thumbnailSize={stackedAsset.id === asset.id ? stackSelectedThumbnailSize : stackThumbnailSize} showStackedIcon={false} disableLinkMouseOver /> - {#if stackedAsset.id === asset.id} -
-
-
- {/if} +
+
+
{/each} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 7cb486c5a6..f88c2d0108 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -18,13 +18,7 @@ import { handleError } from '$lib/utils/handle-error'; import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util'; 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, modalManager, Text } from '@immich/ui'; import { mdiCalendar, @@ -50,9 +44,10 @@ interface Props { asset: AssetResponseDto; currentAlbum?: AlbumResponseDto | null; + onRefreshPeople?: () => Promise; } - let { asset, currentAlbum = null }: Props = $props(); + let { asset, currentAlbum = null, onRefreshPeople }: Props = $props(); let showEditFaces = $derived(assetViewerManager.isEditFacesPanelOpen); let isOwner = $derived(authManager.authenticated && authManager.user.id === asset.ownerId); @@ -117,11 +112,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) }); @@ -573,6 +563,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/face-editor/face-editor.svelte b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte index 2418e5bf85..c1f9440b5e 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 @@ -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/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index da00980f08..b7572e4ac6 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.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); @@ -285,6 +286,12 @@ {#if assetViewerManager.isFaceEditMode && assetViewerManager.imgRef} - + {/if} 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 fa5afadfa6..375685db4e 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -179,7 +179,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')); }