Compare commits

..

1 Commits

Author SHA1 Message Date
Min Idzelis a29dc703b6 refactor(web): expose scaled image dimensions from AdaptiveImage
Change-Id: Iae105fb749525739ba8df5b944a73ea66a6a6964
2026-06-03 03:56:29 +00:00
6 changed files with 71 additions and 79 deletions
+20 -2
View File
@@ -74,6 +74,8 @@
onError?: () => void;
ref?: HTMLDivElement;
imgRef?: HTMLImageElement;
imgNaturalSize?: Size;
imgScaledSize?: Size;
backdrop?: Snippet;
overlays?: Snippet;
};
@@ -82,6 +84,10 @@
ref = $bindable(),
// eslint-disable-next-line no-useless-assignment
imgRef = $bindable(),
// eslint-disable-next-line no-useless-assignment
imgNaturalSize = $bindable(),
// eslint-disable-next-line no-useless-assignment
imgScaledSize = $bindable(),
asset,
sharedLink,
objectFit = 'contain',
@@ -149,10 +155,22 @@
return { width: 1, height: 1 };
});
$effect(() => {
imgNaturalSize = imageDimensions;
});
const scaledDimensions = $derived.by(() => {
const scaleFn = objectFit === 'cover' ? scaleToCover : scaleToFit;
return scaleFn(imageDimensions, container);
});
$effect(() => {
imgScaledSize = scaledDimensions;
});
const { insetInlineStart, top, displayWidth, displayHeight, rasterWidth, rasterHeight, rasterScale } = $derived.by(
() => {
const scaleFn = objectFit === 'cover' ? scaleToCover : scaleToFit;
const { width, height } = scaleFn(imageDimensions, container);
const { width, height } = scaledDimensions;
if (maxRasterPixels === 0) {
return {
insetInlineStart: (container.width - width) / 2 + 'px',
@@ -10,7 +10,6 @@
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';
@@ -103,10 +102,9 @@
const stackSelectedThumbnailSize = 65;
let previewStackedAsset: AssetResponseDto | undefined = $state();
let stack: StackResponseDto | undefined = $state();
let selectedStackAsset: AssetResponseDto | undefined = $state();
let stack: StackResponseDto | null = $state(null);
const asset = $derived(previewStackedAsset ?? selectedStackAsset ?? cursor.current);
const asset = $derived(previewStackedAsset ?? cursor.current);
const nextAsset = $derived(cursor.nextAsset);
const previousAsset = $derived(cursor.previousAsset);
let sharedLink = getSharedLink();
@@ -119,29 +117,17 @@
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 (!cursor.current.stack) {
stack = undefined;
selectedStackAsset = undefined;
return;
if (asset.stack) {
stack = await getStack({ id: asset.stack.id });
}
stack = await getStack({ id: cursor.current.stack.id });
const primaryAsset = stack?.assets.find(({ id }) => id === stack?.primaryAssetId);
if (primaryAsset) {
await selectStackedAsset(primaryAsset.id);
if (!stack?.assets.some(({ id }) => id === asset.id)) {
stack = null;
}
};
@@ -199,21 +185,11 @@
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) {
await refreshPreservingSelection();
const refreshedAsset = await getAssetInfo({ id: asset.id });
onAssetChange?.(refreshedAsset);
assetViewerManager.setAsset(refreshedAsset);
}
assetViewerManager.closeEditor();
};
@@ -328,6 +304,10 @@
}
};
const handleStackedAssetMouseEvent = (isMouseOver: boolean, stackedAsset: AssetResponseDto) => {
previewStackedAsset = isMouseOver ? stackedAsset : undefined;
};
const handlePreAction = (action: Action) => {
preAction?.(action);
};
@@ -340,7 +320,7 @@
break;
}
case AssetAction.REMOVE_ASSET_FROM_STACK: {
stack = action.stack ?? undefined;
stack = action.stack;
if (stack) {
cursor.current = stack.assets[0];
}
@@ -348,7 +328,7 @@
}
case AssetAction.STACK:
case AssetAction.SET_STACK_PRIMARY_ASSET: {
stack = action.stack ?? undefined;
stack = action.stack;
break;
}
case AssetAction.SET_PERSON_FEATURED_PHOTO: {
@@ -411,7 +391,7 @@
$effect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
cursor.current;
asset;
untrack(() => handlePromiseError(refresh()));
});
@@ -580,12 +560,7 @@
{:else if viewerKind === 'CropArea'}
<CropArea {asset} />
{:else if viewerKind === 'PhotoViewer'}
<PhotoViewer
cursor={{ ...cursor, current: asset }}
{sharedLink}
{onSwipe}
onTagFace={refreshPreservingSelection}
/>
<PhotoViewer cursor={{ ...cursor, current: asset }} {sharedLink} {onSwipe} />
{:else if viewerKind === 'VideoViewer'}
<VideoViewer
{asset}
@@ -642,7 +617,7 @@
translate="yes"
>
{#if showDetailPanel}
<DetailPanel {asset} currentAlbum={album} onRefreshPeople={refreshPreservingSelection} />
<DetailPanel {asset} currentAlbum={album} />
{:else if assetViewerManager.isShowEditor}
<EditorPanel {asset} onClose={closeEditor} />
{/if}
@@ -654,24 +629,27 @@
<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">
{#each stackedAssets as stackedAsset (stackedAsset.id)}
{@const isSelected = stackedAsset.id === (selectedStackAsset?.id ?? cursor.current.id)}
<div
class={['pointer-events-auto relative inline-block px-1 pb-2 transition-all']}
style:bottom={isSelected ? '0' : '-10px'}
style:bottom={stackedAsset.id === asset.id ? '0' : '-10px'}
>
<Thumbnail
imageClass={{ 'border-2 border-white': isSelected }}
imageClass={{ 'border-2 border-white': stackedAsset.id === asset.id }}
brokenAssetClass="text-xs"
dimmed={!isSelected}
dimmed={stackedAsset.id !== asset.id}
asset={toTimelineAsset(stackedAsset)}
onClick={() => selectStackedAsset(stackedAsset.id)}
onClick={() => {
cursor.current = stackedAsset;
previewStackedAsset = undefined;
}}
onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)}
readonly
thumbnailSize={isSelected ? stackSelectedThumbnailSize : stackThumbnailSize}
thumbnailSize={stackedAsset.id === asset.id ? stackSelectedThumbnailSize : stackThumbnailSize}
showStackedIcon={false}
disableLinkMouseOver
/>
{#if isSelected}
{#if stackedAsset.id === asset.id}
<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>
@@ -16,7 +16,13 @@
import { getByteUnitString } from '$lib/utils/byte-units';
import { handleError } from '$lib/utils/handle-error';
import { getParentPath } from '$lib/utils/tree-utils';
import { AssetMediaSize, getAllAlbums, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk';
import {
AssetMediaSize,
getAllAlbums,
getAssetInfo,
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';
@@ -31,10 +37,9 @@
interface Props {
asset: AssetResponseDto;
currentAlbum?: AlbumResponseDto | null;
onRefreshPeople?: () => Promise<void>;
}
let { asset, currentAlbum = null, onRefreshPeople }: Props = $props();
let { asset, currentAlbum = null }: Props = $props();
let isOwner = $derived(authManager.authenticated && authManager.user.id === asset.ownerId);
let latlng = $derived(
@@ -89,6 +94,11 @@
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) });
@@ -375,6 +385,6 @@
assetId={asset.id}
assetType={asset.type}
onClose={() => assetViewerManager.closeEditFacesPanel()}
onRefresh={() => void onRefreshPeople?.()}
onRefresh={handleRefreshPeople}
/>
{/if}
@@ -13,7 +13,7 @@
import { SlideshowLook, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { handlePromiseError } from '$lib/utils';
import { canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils';
import { getNaturalSize, scaleToFit, type Size } from '$lib/utils/container-utils';
import type { Size } from '$lib/utils/container-utils';
import { handleError } from '$lib/utils/handle-error';
import { getOcrBoundingBoxes } from '$lib/utils/ocr-utils';
import { getBoundingBox, type BoundingBox } from '$lib/utils/people-utils';
@@ -31,10 +31,9 @@
onReady?: () => void;
onError?: () => void;
onSwipe?: (event: SwipeCustomEvent) => void;
onTagFace?: () => Promise<void>;
};
let { cursor, element = $bindable(), sharedLink, onReady, onError, onSwipe, onTagFace }: Props = $props();
let { cursor, element = $bindable(), sharedLink, onReady, onError, onSwipe }: Props = $props();
const { slideshowState, slideshowLook } = slideshowStore;
const asset = $derived(cursor.current);
@@ -68,13 +67,9 @@
height: containerHeight,
});
const overlaySize = $derived.by((): Size => {
if (!assetViewerManager.imgRef || !visibleImageReady) {
return { width: 0, height: 0 };
}
let scaledDimensions = $state<Size>({ width: 0, height: 0 });
return scaleToFit(getNaturalSize(assetViewerManager.imgRef), { width: containerWidth, height: containerHeight });
});
const overlaySize = $derived(visibleImageReady ? scaledDimensions : { width: 0, height: 0 });
const highlightedBoxes = $derived(getBoundingBox(assetViewerManager.highlightedFaces, overlaySize));
const isHighlighting = $derived(highlightedBoxes.length > 0);
@@ -236,6 +231,7 @@
onReady?.();
}}
bind:imgRef={assetViewerManager.imgRef}
bind:imgScaledSize={scaledDimensions}
bind:ref={adaptiveImage}
>
{#snippet backdrop()}
@@ -288,12 +284,6 @@
</AdaptiveImage>
{#if assetViewerManager.isFaceEditMode && assetViewerManager.imgRef}
<FaceEditor
htmlElement={assetViewerManager.imgRef}
{containerWidth}
{containerHeight}
assetId={asset.id}
{onTagFace}
/>
<FaceEditor htmlElement={assetViewerManager.imgRef} {containerWidth} {containerHeight} assetId={asset.id} />
{/if}
</div>
@@ -18,10 +18,9 @@
containerWidth: number;
containerHeight: number;
assetId: string;
onTagFace?: () => Promise<void>;
};
let { htmlElement, containerWidth, containerHeight, assetId, onTagFace }: Props = $props();
let { htmlElement, containerWidth, containerHeight, assetId }: Props = $props();
let canvasEl: HTMLCanvasElement | undefined = $state();
let canvas: Canvas | undefined = $state();
@@ -326,7 +325,7 @@
},
});
await onTagFace?.();
await assetViewerManager.setAssetId(assetId);
} catch (error) {
handleError(error, 'Error tagging face');
} finally {
@@ -178,10 +178,7 @@
peopleWithFaces = peopleWithFaces.filter((f) => f.id !== face.id);
onRefresh();
if (peopleWithFaces.length === 0) {
onClose();
}
await assetViewerManager.setAssetId(assetId);
} catch (error) {
handleError(error, $t('error_delete_face'));
}