From 824be9f228265c71e2695cda5c3081eff98c848d Mon Sep 17 00:00:00 2001 From: midzelis Date: Fri, 6 Mar 2026 20:19:07 +0000 Subject: [PATCH] fix z-ordering, refresh selected in stacked viewer, ocr/face in stacked viewer --- e2e/src/specs/web/photo-viewer.e2e-spec.ts | 15 +- .../asset-viewer/broken-asset.e2e-spec.ts | 2 +- web/src/lib/components/AdaptiveImage.svelte | 8 +- .../asset-viewer/PreloadManager.svelte.ts | 6 +- .../asset-viewer/asset-viewer.svelte | 128 +++++--- .../asset-viewer/detail-panel.svelte | 18 +- .../face-editor/face-editor.svelte | 6 +- .../asset-viewer/photo-viewer.svelte | 11 +- .../faces-page/person-side-panel.svelte | 8 +- web/src/lib/managers/ImageManager.svelte.ts | 62 ---- .../lib/utils/adaptive-image-loader.spec.ts | 304 ++++++++++++++++++ .../lib/utils/adaptive-image-loader.svelte.ts | 37 +-- 12 files changed, 423 insertions(+), 182 deletions(-) delete mode 100644 web/src/lib/managers/ImageManager.svelte.ts create mode 100644 web/src/lib/utils/adaptive-image-loader.spec.ts diff --git a/e2e/src/specs/web/photo-viewer.e2e-spec.ts b/e2e/src/specs/web/photo-viewer.e2e-spec.ts index 1e723916d3..88b61278bc 100644 --- a/e2e/src/specs/web/photo-viewer.e2e-spec.ts +++ b/e2e/src/specs/web/photo-viewer.e2e-spec.ts @@ -31,8 +31,8 @@ test.describe('Photo Viewer', () => { test('loads original photo when zoomed', async ({ page }) => { await page.goto(`/photos/${asset.id}`); - const thumbnail = page.getByTestId('thumbnail').filter({ visible: true }); - await expect(thumbnail).toHaveAttribute('src', /thumbnail/); + const preview = page.getByTestId('preview').filter({ visible: true }); + await expect(preview).toHaveAttribute('src', /.+/); const originalResponse = page.waitForResponse((response) => response.url().includes('/original')); @@ -49,8 +49,8 @@ test.describe('Photo Viewer', () => { test('loads fullsize image when zoomed and original is web-incompatible', async ({ page }) => { await page.goto(`/photos/${rawAsset.id}`); - const thumbnail = page.getByTestId('thumbnail').filter({ visible: true }); - await expect(thumbnail).toHaveAttribute('src', /thumbnail/); + const preview = page.getByTestId('preview').filter({ visible: true }); + await expect(preview).toHaveAttribute('src', /.+/); const fullsizeResponse = page.waitForResponse((response) => response.url().includes('fullsize')); @@ -67,15 +67,14 @@ test.describe('Photo Viewer', () => { test('reloads photo when checksum changes', async ({ page }) => { await page.goto(`/photos/${asset.id}`); - const thumbnail = page.getByTestId('thumbnail').filter({ visible: true }); - await expect(thumbnail).toHaveAttribute('src', /thumbnail/); - const initialSrc = await thumbnail.getAttribute('src'); + const preview = page.getByTestId('preview').filter({ visible: true }); + await expect(preview).toHaveAttribute('src', /.+/); + const initialSrc = await preview.getAttribute('src'); const websocketEvent = utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id }); await utils.replaceAsset(admin.accessToken, asset.id); await websocketEvent; - const preview = page.getByTestId('preview').filter({ visible: true }); await expect(preview).not.toHaveAttribute('src', initialSrc!); }); }); diff --git a/e2e/src/ui/specs/asset-viewer/broken-asset.e2e-spec.ts b/e2e/src/ui/specs/asset-viewer/broken-asset.e2e-spec.ts index b2502df6fc..de3460f893 100644 --- a/e2e/src/ui/specs/asset-viewer/broken-asset.e2e-spec.ts +++ b/e2e/src/ui/specs/asset-viewer/broken-asset.e2e-spec.ts @@ -73,7 +73,7 @@ test.describe('broken-asset responsiveness', () => { await page.goto(`/photos/${fixture.primaryAsset.id}`); await page.waitForSelector('#immich-asset-viewer'); - const viewerBrokenAsset = page.locator('[data-viewer-content] [data-broken-asset]'); + const viewerBrokenAsset = page.locator('[data-viewer-content] [data-broken-asset]').first(); await expect(viewerBrokenAsset).toBeVisible(); await expect(viewerBrokenAsset.locator('svg')).toBeVisible(); diff --git a/web/src/lib/components/AdaptiveImage.svelte b/web/src/lib/components/AdaptiveImage.svelte index 20dc5d4a2f..0e58c98f01 100644 --- a/web/src/lib/components/AdaptiveImage.svelte +++ b/web/src/lib/components/AdaptiveImage.svelte @@ -59,17 +59,15 @@ { quality: 'thumbnail', url: assetUrls.thumbnail, - checkCanceled: false, onAfterLoad: afterThumbnail, onAfterError: afterThumbnail, }, { quality: 'preview', url: assetUrls.preview, - checkCanceled: true, onAfterError: (loader) => loader.trigger('original'), }, - { quality: 'original', url: assetUrls.original, checkCanceled: true }, + { quality: 'original', url: assetUrls.original }, ]; return qualityList; }; @@ -81,7 +79,7 @@ return untrack( () => - new AdaptiveImageLoader(asset.id, buildQualityList(), { + new AdaptiveImageLoader(buildQualityList(), { onImageReady, onError, onUrlChange, @@ -131,7 +129,7 @@ }); $effect(() => { - if (assetViewerManager.zoom > 1 && status.quality.preview === 'success' && status.quality.original !== 'success') { + if (assetViewerManager.zoom > 1 && status.quality.original !== 'success') { untrack(() => void adaptiveImageLoader.trigger('original')); } }); diff --git a/web/src/lib/components/asset-viewer/PreloadManager.svelte.ts b/web/src/lib/components/asset-viewer/PreloadManager.svelte.ts index 57935a4be8..60328abe8f 100644 --- a/web/src/lib/components/asset-viewer/PreloadManager.svelte.ts +++ b/web/src/lib/components/asset-viewer/PreloadManager.svelte.ts @@ -23,19 +23,17 @@ export class PreloadManager { { quality: 'thumbnail', url: urls.thumbnail, - checkCanceled: false, onAfterLoad: afterThumbnail, onAfterError: afterThumbnail, }, { quality: 'preview', url: urls.preview, - checkCanceled: true, onAfterError: (loader) => loader.trigger('original'), }, - { quality: 'original', url: urls.original, checkCanceled: true }, + { quality: 'original', url: urls.original }, ]; - const loader = new AdaptiveImageLoader(asset.id, qualityList, undefined, loadImage); + const loader = new AdaptiveImageLoader(qualityList, undefined, loadImage); loader.start(); return loader; } diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 69b6a4f6b2..ee18d24e89 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -9,6 +9,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'; @@ -46,6 +47,7 @@ import DetailPanel from './detail-panel.svelte'; import EditorPanel from './editor/editor-panel.svelte'; import CropArea from './editor/transform-tool/crop-area.svelte'; + import FaceEditor from './face-editor/face-editor.svelte'; import ImagePanoramaViewer from './image-panorama-viewer.svelte'; import OcrButton from './ocr-button.svelte'; import PhotoViewer from './photo-viewer.svelte'; @@ -116,6 +118,10 @@ playOriginalVideo = value; }; + const selectStackedAsset = async (id: string) => { + selectedStackAsset = await assetCacheManager.getAsset({ id }); + }; + const refreshStack = async () => { if (authManager.isSharedLink || !withStacked) { return; @@ -128,7 +134,10 @@ } stack = await getStack({ id: cursor.current.stack.id }); - selectedStackAsset = stack?.assets.find(({ id }) => id === stack?.primaryAssetId); + const primaryAsset = stack?.assets.find(({ id }) => id === stack?.primaryAssetId); + if (primaryAsset) { + await selectStackedAsset(primaryAsset.id); + } }; const handleFavorite = async () => { @@ -179,11 +188,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 }); + assetViewingStore.setAsset(asset); + } + onAssetChange?.(asset); + }; + const closeEditor = async () => { if (editManager.hasAppliedEdits) { - const refreshedAsset = await getAssetInfo({ id: asset.id }); - onAssetChange?.(refreshedAsset); - assetViewingStore.setAsset(refreshedAsset); + await refreshPreservingSelection(); } assetViewerManager.closeEditor(); }; @@ -454,6 +473,9 @@ navigateAsset('previous'); } }; + + let containerWidth = $state(0); + let containerHeight = $state(0); @@ -465,6 +487,8 @@ class="fixed start-0 top-0 grid size-full grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black" use:focusTrap bind:this={assetViewerHtmlElement} + bind:clientWidth={containerWidth} + bind:clientHeight={containerHeight} > {#if $slideshowState === SlideshowState.None && !assetViewerManager.isShowEditor} @@ -570,6 +594,62 @@ {/if} + + {#if stack && withStacked && !assetViewerManager.isShowEditor} + {@const stackedAssets = stack.assets} +
+ +
+ {/if} + + {#if isFaceEditMode.value && assetViewerManager.imgRef} + + {/if} {#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && !isFaceEditMode.value && nextAsset} @@ -587,7 +667,7 @@ > {#if showDetailPanel}
- +
{:else if assetViewerManager.isShowEditor}
@@ -597,44 +677,6 @@
{/if} - {#if stack && withStacked && !assetViewerManager.isShowEditor} - {@const stackedAssets = stack.assets} -
- -
- {/if} - {#if isShared && album && assetViewerManager.isShowActivityPanel && $user}
Promise; } - let { asset, currentAlbum = null }: Props = $props(); + let { asset, currentAlbum = null, onRefreshPeople }: Props = $props(); let showAssetPath = $state(false); let showEditFaces = $state(false); @@ -120,11 +115,6 @@ return undefined; }; - const handleRefreshPeople = async () => { - asset = await getAssetInfo({ id: asset.id }); - showEditFaces = false; - }; - const getAssetFolderHref = (asset: AssetResponseDto) => { // Remove the last part of the path to get the parent path return Route.folders({ path: getParentPath(asset.originalPath) }); @@ -575,6 +565,6 @@ assetId={asset.id} assetType={asset.type} onClose={() => (showEditFaces = false)} - 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 e84bc9fa0c..782306edbb 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 @@ -1,6 +1,5 @@