mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
fix(web): preserve stacked asset selection when tagging faces
Change-Id: Iec1507560f99f2e9433bd5cf6b460b176a6a6964
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import { AssetAction, ProjectionType } from '$lib/constants';
|
import { AssetAction, ProjectionType } from '$lib/constants';
|
||||||
import { activityManager } from '$lib/managers/activity-manager.svelte';
|
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 { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { editManager, EditToolType } from '$lib/managers/edit/edit-manager.svelte';
|
import { editManager, EditToolType } from '$lib/managers/edit/edit-manager.svelte';
|
||||||
@@ -99,9 +100,10 @@
|
|||||||
const stackSelectedThumbnailSize = 65;
|
const stackSelectedThumbnailSize = 65;
|
||||||
|
|
||||||
let previewStackedAsset: AssetResponseDto | undefined = $state();
|
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 nextAsset = $derived(cursor.nextAsset);
|
||||||
const previousAsset = $derived(cursor.previousAsset);
|
const previousAsset = $derived(cursor.previousAsset);
|
||||||
let sharedLink = getSharedLink();
|
let sharedLink = getSharedLink();
|
||||||
@@ -114,17 +116,29 @@
|
|||||||
playOriginalVideo = value;
|
playOriginalVideo = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectStackedAsset = async (id: string) => {
|
||||||
|
ocrManager.clear();
|
||||||
|
selectedStackAsset = await assetCacheManager.getAsset({ id });
|
||||||
|
if (!sharedLink) {
|
||||||
|
await ocrManager.getAssetOcr(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const refreshStack = async () => {
|
const refreshStack = async () => {
|
||||||
if (authManager.isSharedLink || !withStacked) {
|
if (authManager.isSharedLink || !withStacked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asset.stack) {
|
if (!cursor.current.stack) {
|
||||||
stack = await getStack({ id: asset.stack.id });
|
stack = undefined;
|
||||||
|
selectedStackAsset = undefined;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stack?.assets.some(({ id }) => id === asset.id)) {
|
stack = await getStack({ id: cursor.current.stack.id });
|
||||||
stack = null;
|
const primaryAsset = stack?.assets.find(({ id }) => id === stack?.primaryAssetId);
|
||||||
|
if (primaryAsset) {
|
||||||
|
await selectStackedAsset(primaryAsset.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -182,11 +196,21 @@
|
|||||||
onClose?.(asset.id);
|
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 () => {
|
const closeEditor = async () => {
|
||||||
if (editManager.hasAppliedEdits) {
|
if (editManager.hasAppliedEdits) {
|
||||||
const refreshedAsset = await getAssetInfo({ id: asset.id });
|
await refreshPreservingSelection();
|
||||||
onAssetChange?.(refreshedAsset);
|
|
||||||
assetViewerManager.setAsset(refreshedAsset);
|
|
||||||
}
|
}
|
||||||
assetViewerManager.closeEditor();
|
assetViewerManager.closeEditor();
|
||||||
};
|
};
|
||||||
@@ -285,10 +309,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStackedAssetMouseEvent = (isMouseOver: boolean, stackedAsset: AssetResponseDto) => {
|
|
||||||
previewStackedAsset = isMouseOver ? stackedAsset : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePreAction = (action: Action) => {
|
const handlePreAction = (action: Action) => {
|
||||||
preAction?.(action);
|
preAction?.(action);
|
||||||
};
|
};
|
||||||
@@ -301,7 +321,7 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AssetAction.REMOVE_ASSET_FROM_STACK: {
|
case AssetAction.REMOVE_ASSET_FROM_STACK: {
|
||||||
stack = action.stack;
|
stack = action.stack ?? undefined;
|
||||||
if (stack) {
|
if (stack) {
|
||||||
cursor.current = stack.assets[0];
|
cursor.current = stack.assets[0];
|
||||||
}
|
}
|
||||||
@@ -309,7 +329,7 @@
|
|||||||
}
|
}
|
||||||
case AssetAction.STACK:
|
case AssetAction.STACK:
|
||||||
case AssetAction.SET_STACK_PRIMARY_ASSET: {
|
case AssetAction.SET_STACK_PRIMARY_ASSET: {
|
||||||
stack = action.stack;
|
stack = action.stack ?? undefined;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AssetAction.SET_PERSON_FEATURED_PHOTO: {
|
case AssetAction.SET_PERSON_FEATURED_PHOTO: {
|
||||||
@@ -368,7 +388,7 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
asset;
|
cursor.current;
|
||||||
untrack(() => handlePromiseError(refresh()));
|
untrack(() => handlePromiseError(refresh()));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -533,7 +553,12 @@
|
|||||||
{:else if viewerKind === 'CropArea'}
|
{:else if viewerKind === 'CropArea'}
|
||||||
<CropArea {asset} />
|
<CropArea {asset} />
|
||||||
{:else if viewerKind === 'PhotoViewer'}
|
{:else if viewerKind === 'PhotoViewer'}
|
||||||
<PhotoViewer cursor={{ ...cursor, current: asset }} {sharedLink} {onSwipe} />
|
<PhotoViewer
|
||||||
|
cursor={{ ...cursor, current: asset }}
|
||||||
|
{sharedLink}
|
||||||
|
{onSwipe}
|
||||||
|
onTagFace={refreshPreservingSelection}
|
||||||
|
/>
|
||||||
{:else if viewerKind === 'VideoViewer'}
|
{:else if viewerKind === 'VideoViewer'}
|
||||||
<VideoViewer
|
<VideoViewer
|
||||||
{asset}
|
{asset}
|
||||||
@@ -586,7 +611,7 @@
|
|||||||
translate="yes"
|
translate="yes"
|
||||||
>
|
>
|
||||||
{#if showDetailPanel}
|
{#if showDetailPanel}
|
||||||
<DetailPanel {asset} currentAlbum={album} />
|
<DetailPanel {asset} currentAlbum={album} onRefreshPeople={refreshPreservingSelection} />
|
||||||
{:else if assetViewerManager.isShowEditor}
|
{:else if assetViewerManager.isShowEditor}
|
||||||
<EditorPanel {asset} onClose={closeEditor} />
|
<EditorPanel {asset} onClose={closeEditor} />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -598,27 +623,24 @@
|
|||||||
<div id="stack-slideshow" class="pointer-events-none absolute bottom-0 col-span-4 col-start-1 w-full">
|
<div id="stack-slideshow" class="pointer-events-none absolute bottom-0 col-span-4 col-start-1 w-full">
|
||||||
<div class="no-wrap horizontal-scrollbar relative flex flex-row overflow-x-auto overflow-y-hidden">
|
<div class="no-wrap horizontal-scrollbar relative flex flex-row overflow-x-auto overflow-y-hidden">
|
||||||
{#each stackedAssets as stackedAsset (stackedAsset.id)}
|
{#each stackedAssets as stackedAsset (stackedAsset.id)}
|
||||||
|
{@const isSelected = stackedAsset.id === (selectedStackAsset?.id ?? cursor.current.id)}
|
||||||
<div
|
<div
|
||||||
class={['pointer-events-auto relative inline-block px-1 pb-2 transition-all']}
|
class={['pointer-events-auto relative inline-block px-1 pb-2 transition-all']}
|
||||||
style:bottom={stackedAsset.id === asset.id ? '0' : '-10px'}
|
style:bottom={isSelected ? '0' : '-10px'}
|
||||||
>
|
>
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
imageClass={{ 'border-2 border-white': stackedAsset.id === asset.id }}
|
imageClass={{ 'border-2 border-white': isSelected }}
|
||||||
brokenAssetClass="text-xs"
|
brokenAssetClass="text-xs"
|
||||||
dimmed={stackedAsset.id !== asset.id}
|
dimmed={!isSelected}
|
||||||
asset={toTimelineAsset(stackedAsset)}
|
asset={toTimelineAsset(stackedAsset)}
|
||||||
onClick={() => {
|
onClick={() => selectStackedAsset(stackedAsset.id)}
|
||||||
cursor.current = stackedAsset;
|
|
||||||
previewStackedAsset = undefined;
|
|
||||||
}}
|
|
||||||
onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
|
|
||||||
readonly
|
readonly
|
||||||
thumbnailSize={stackedAsset.id === asset.id ? stackSelectedThumbnailSize : stackThumbnailSize}
|
thumbnailSize={isSelected ? stackSelectedThumbnailSize : stackThumbnailSize}
|
||||||
showStackedIcon={false}
|
showStackedIcon={false}
|
||||||
disableLinkMouseOver
|
disableLinkMouseOver
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if stackedAsset.id === asset.id}
|
{#if isSelected}
|
||||||
<div class="flex w-full place-content-center place-items-center">
|
<div class="flex w-full place-content-center place-items-center">
|
||||||
<div class="mt-0.5 flex size-2 rounded-full bg-white"></div>
|
<div class="mt-0.5 flex size-2 rounded-full bg-white"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,13 +16,7 @@
|
|||||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getParentPath } from '$lib/utils/tree-utils';
|
import { getParentPath } from '$lib/utils/tree-utils';
|
||||||
import {
|
import { AssetMediaSize, getAllAlbums, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk';
|
||||||
AssetMediaSize,
|
|
||||||
getAllAlbums,
|
|
||||||
getAssetInfo,
|
|
||||||
type AlbumResponseDto,
|
|
||||||
type AssetResponseDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { Icon, IconButton, LoadingSpinner, Text } from '@immich/ui';
|
import { Icon, IconButton, LoadingSpinner, Text } from '@immich/ui';
|
||||||
import { mdiCamera, mdiCameraIris, mdiClose, mdiImageOutline, mdiInformationOutline } from '@mdi/js';
|
import { mdiCamera, mdiCameraIris, mdiClose, mdiImageOutline, mdiInformationOutline } from '@mdi/js';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
@@ -37,9 +31,10 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
currentAlbum?: AlbumResponseDto | null;
|
currentAlbum?: AlbumResponseDto | null;
|
||||||
|
onRefreshPeople?: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { asset, currentAlbum = null }: Props = $props();
|
let { asset, currentAlbum = null, onRefreshPeople }: Props = $props();
|
||||||
|
|
||||||
let isOwner = $derived(authManager.authenticated && authManager.user.id === asset.ownerId);
|
let isOwner = $derived(authManager.authenticated && authManager.user.id === asset.ownerId);
|
||||||
let latlng = $derived(
|
let latlng = $derived(
|
||||||
@@ -94,11 +89,6 @@
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefreshPeople = async () => {
|
|
||||||
asset = await getAssetInfo({ id: asset.id });
|
|
||||||
assetViewerManager.closeEditFacesPanel();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAssetFolderHref = (asset: AssetResponseDto) => {
|
const getAssetFolderHref = (asset: AssetResponseDto) => {
|
||||||
// Remove the last part of the path to get the parent path
|
// Remove the last part of the path to get the parent path
|
||||||
return Route.folders({ path: getParentPath(asset.originalPath) });
|
return Route.folders({ path: getParentPath(asset.originalPath) });
|
||||||
@@ -385,6 +375,6 @@
|
|||||||
assetId={asset.id}
|
assetId={asset.id}
|
||||||
assetType={asset.type}
|
assetType={asset.type}
|
||||||
onClose={() => assetViewerManager.closeEditFacesPanel()}
|
onClose={() => assetViewerManager.closeEditFacesPanel()}
|
||||||
onRefresh={handleRefreshPeople}
|
onRefresh={() => void onRefreshPeople?.()}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -30,9 +30,10 @@
|
|||||||
onReady?: () => void;
|
onReady?: () => void;
|
||||||
onError?: () => void;
|
onError?: () => void;
|
||||||
onSwipe?: (event: SwipeCustomEvent) => void;
|
onSwipe?: (event: SwipeCustomEvent) => void;
|
||||||
|
onTagFace?: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
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 { slideshowState, slideshowLook } = slideshowStore;
|
||||||
const asset = $derived(cursor.current);
|
const asset = $derived(cursor.current);
|
||||||
@@ -285,6 +286,12 @@
|
|||||||
</AdaptiveImage>
|
</AdaptiveImage>
|
||||||
|
|
||||||
{#if assetViewerManager.isFaceEditMode && assetViewerManager.imgRef}
|
{#if assetViewerManager.isFaceEditMode && assetViewerManager.imgRef}
|
||||||
<FaceEditor htmlElement={assetViewerManager.imgRef} {containerWidth} {containerHeight} assetId={asset.id} />
|
<FaceEditor
|
||||||
|
htmlElement={assetViewerManager.imgRef}
|
||||||
|
{containerWidth}
|
||||||
|
{containerHeight}
|
||||||
|
assetId={asset.id}
|
||||||
|
{onTagFace}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,9 +18,10 @@
|
|||||||
containerWidth: number;
|
containerWidth: number;
|
||||||
containerHeight: number;
|
containerHeight: number;
|
||||||
assetId: string;
|
assetId: string;
|
||||||
|
onTagFace?: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { htmlElement, containerWidth, containerHeight, assetId }: Props = $props();
|
let { htmlElement, containerWidth, containerHeight, assetId, onTagFace }: Props = $props();
|
||||||
|
|
||||||
let canvasEl: HTMLCanvasElement | undefined = $state();
|
let canvasEl: HTMLCanvasElement | undefined = $state();
|
||||||
let canvas: Canvas | undefined = $state();
|
let canvas: Canvas | undefined = $state();
|
||||||
@@ -325,7 +326,7 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await assetViewerManager.setAssetId(assetId);
|
await onTagFace?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Error tagging face');
|
handleError(error, 'Error tagging face');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -178,7 +178,10 @@
|
|||||||
|
|
||||||
peopleWithFaces = peopleWithFaces.filter((f) => f.id !== face.id);
|
peopleWithFaces = peopleWithFaces.filter((f) => f.id !== face.id);
|
||||||
|
|
||||||
await assetViewerManager.setAssetId(assetId);
|
onRefresh();
|
||||||
|
if (peopleWithFaces.length === 0) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('error_delete_face'));
|
handleError(error, $t('error_delete_face'));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user