feat(web): face overlay hover UX and face editor zoom preservation

Change-Id: I7164305d7764bec54fa06b8738cd97fd6a6a6964

refactor(web): use asset metadata for face editor image dimensions instead of DOM

The face editor previously read naturalWidth/naturalHeight from the DOM
element via a $effect + load event listener. This was fragile on slow
hardware (ARM CI) because imgRef changes as AdaptiveImage progresses
through quality levels, and the DOM element's natural dimensions could
be 0 during transitions.

Now the face editor receives imageSize as a prop from the parent, derived
from the asset's metadata dimensions which are always available immediately.

Change-Id: Id4c3a59110feff4c50f429bbd744eac46a6a6964
Change-Id: I7164305d7764bec54fa06b8738cd97fd6a6a6964
This commit is contained in:
midzelis
2026-03-19 13:02:46 +00:00
parent ed04d87273
commit 590a9df7ec
21 changed files with 1147 additions and 208 deletions

View File

@@ -59,7 +59,7 @@
previousAsset?: AssetResponseDto;
};
interface Props {
type Props = {
cursor: AssetCursor;
showNavigation?: boolean;
withStacked?: boolean;
@@ -72,7 +72,7 @@
onUndoDelete?: OnUndoDelete;
onClose?: (asset: AssetResponseDto) => void;
onRandom?: () => Promise<{ id: string } | undefined>;
}
};
let {
cursor,
@@ -176,6 +176,7 @@
onDestroy(() => {
activityManager.reset();
assetViewerManager.closeEditor();
isFaceEditMode.value = false;
syncAssetViewerOpenClass(false);
preloadManager.destroy();
});
@@ -290,6 +291,9 @@
const handleStackedAssetMouseEvent = (isMouseOver: boolean, stackedAsset: AssetResponseDto) => {
previewStackedAsset = isMouseOver ? stackedAsset : undefined;
if (isMouseOver) {
isFaceEditMode.value = false;
}
};
const handlePreAction = (action: Action) => {
@@ -358,15 +362,18 @@
}
};
const refreshOcr = async () => {
ocrManager.clear();
if (sharedLink) {
return;
}
await ocrManager.getAssetOcr(asset.id);
};
const refresh = async () => {
await refreshStack();
ocrManager.clear();
if (!sharedLink) {
if (previewStackedAsset) {
await ocrManager.getAssetOcr(previewStackedAsset.id);
}
await ocrManager.getAssetOcr(asset.id);
}
await refreshOcr();
};
$effect(() => {
@@ -375,6 +382,12 @@
untrack(() => handlePromiseError(refresh()));
});
$effect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
previewStackedAsset;
untrack(() => ocrManager.clear());
});
let lastCursor = $state<AssetCursor>();
$effect(() => {
@@ -460,7 +473,7 @@
<section
id="immich-asset-viewer"
class="fixed start-0 top-0 grid size-full grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black"
class="fixed start-0 top-0 grid size-full grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black touch-none"
use:focusTrap
bind:this={assetViewerHtmlElement}
>
@@ -612,6 +625,7 @@
onClick={() => {
cursor.current = stackedAsset;
previewStackedAsset = undefined;
isFaceEditMode.value = false;
}}
onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
readonly