diff --git a/e2e/src/specs/web/photo-viewer.e2e-spec.ts b/e2e/src/specs/web/photo-viewer.e2e-spec.ts index 3f9bb4237a188..0918309596a07 100644 --- a/e2e/src/specs/web/photo-viewer.e2e-spec.ts +++ b/e2e/src/specs/web/photo-viewer.e2e-spec.ts @@ -1,10 +1,7 @@ import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk'; -import { Page, expect, test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { utils } from 'src/utils'; -function imageLocator(page: Page) { - return page.getByAltText('Image taken').locator('visible=true'); -} test.describe('Photo Viewer', () => { let admin: LoginResponseDto; let asset: AssetMediaResponseDto; @@ -26,31 +23,32 @@ test.describe('Photo Viewer', () => { test('loads original photo when zoomed', async ({ page }) => { await page.goto(`/photos/${asset.id}`); - await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail'); - const box = await imageLocator(page).boundingBox(); + await expect(page.getByTestId('thumbnail')).toHaveAttribute('src', /thumbnail/); + const box = await page.getByTestId('thumbnail').boundingBox(); expect(box).toBeTruthy(); const { x, y, width, height } = box!; await page.mouse.move(x + width / 2, y + height / 2); await page.mouse.wheel(0, -1); - await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('original'); + await expect(page.getByTestId('original')).toBeInViewport(); + await expect(page.getByTestId('original')).toHaveAttribute('src', /original/); }); test('loads fullsize image when zoomed and original is web-incompatible', async ({ page }) => { await page.goto(`/photos/${rawAsset.id}`); - await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail'); - const box = await imageLocator(page).boundingBox(); + await expect(page.getByTestId('thumbnail')).toHaveAttribute('src', /thumbnail/); + const box = await page.getByTestId('thumbnail').boundingBox(); expect(box).toBeTruthy(); const { x, y, width, height } = box!; await page.mouse.move(x + width / 2, y + height / 2); await page.mouse.wheel(0, -1); - await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('fullsize'); + await expect(page.getByTestId('original')).toHaveAttribute('src', /fullsize/); }); test('reloads photo when checksum changes', async ({ page }) => { await page.goto(`/photos/${asset.id}`); - await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail'); - const initialSrc = await imageLocator(page).getAttribute('src'); + await expect(page.getByTestId('thumbnail')).toHaveAttribute('src', /thumbnail/); + const initialSrc = await page.getByTestId('thumbnail').getAttribute('src'); await utils.replaceAsset(admin.accessToken, asset.id); - await expect.poll(async () => await imageLocator(page).getAttribute('src')).not.toBe(initialSrc); + await expect(page.getByTestId('preview')).not.toHaveAttribute('src', initialSrc!); }); }); diff --git a/web/src/lib/components/AdaptiveImage.svelte b/web/src/lib/components/AdaptiveImage.svelte new file mode 100644 index 0000000000000..920e8e5e47dbd --- /dev/null +++ b/web/src/lib/components/AdaptiveImage.svelte @@ -0,0 +1,253 @@ + + +
+ + {#if blurredSlideshow} + + {/if} + + +
+ {#if showAlphaBackground} + + {/if} + + {#if showThumbhash} + {#if asset.thumbhash} + + + {:else if showSpinner} +
+ +
+ {/if} + {/if} + + {#if showThumbnail} + {#key adaptiveImageLoader} + {@const loader = adaptiveImageLoader!} +
+ loader.onThumbnailStart()} + onLoad={() => loader.onThumbnailLoad()} + onError={() => loader.onThumbnailError()} + bind:ref={thumbnailElement} + class={['absolute h-full', 'w-full']} + alt="" + role="presentation" + data-testid="thumbnail" + /> +
+ {/key} + {/if} + + {#if showBrokenAsset} + + {/if} + + {#if showPreview} + {#key adaptiveImageLoader} + {@const loader = adaptiveImageLoader!} +
+ loader.onPreviewStart()} + onLoad={() => loader.onPreviewLoad()} + onError={() => loader.onPreviewError()} + bind:ref={previewElement} + class={['h-full', 'w-full', imageClass]} + alt={imageAltText} + draggable={false} + data-testid="preview" + /> + {@render overlays?.()} +
+ {/key} + {/if} + + {#if showOriginal} + {#key adaptiveImageLoader} + {@const loader = adaptiveImageLoader!} +
+ loader.onOriginalStart()} + onLoad={() => loader.onOriginalLoad()} + onError={() => loader.onOriginalError()} + bind:ref={originalElement} + class={['h-full', 'w-full', imageClass]} + alt={imageAltText} + draggable={false} + data-testid="original" + /> + {@render overlays?.()} +
+ {/key} + {/if} +
+
+ + diff --git a/web/src/lib/components/asset-viewer/actions/action.ts b/web/src/lib/components/asset-viewer/actions/action.ts index 19cc5afa8d540..761887a465d61 100644 --- a/web/src/lib/components/asset-viewer/actions/action.ts +++ b/web/src/lib/components/asset-viewer/actions/action.ts @@ -14,7 +14,7 @@ type ActionMap = { [AssetAction.UNSTACK]: { assets: TimelineAsset[] }; [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: TimelineAsset }; [AssetAction.SET_STACK_PRIMARY_ASSET]: { stack: StackResponseDto }; - [AssetAction.REMOVE_ASSET_FROM_STACK]: { stack: StackResponseDto | null; asset: AssetResponseDto }; + [AssetAction.REMOVE_ASSET_FROM_STACK]: { stack: StackResponseDto | undefined; asset: AssetResponseDto }; [AssetAction.SET_VISIBILITY_LOCKED]: { asset: TimelineAsset }; [AssetAction.SET_VISIBILITY_TIMELINE]: { asset: TimelineAsset }; [AssetAction.SET_PERSON_FEATURED_PHOTO]: { asset: AssetResponseDto; person: PersonResponseDto }; diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 848870b654e2b..7666168d8ffe5 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -1,6 +1,7 @@ @@ -477,7 +568,7 @@ {/if} - {#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && previousAsset} + {#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && !isFaceEditMode.value && previousAsset}
navigateAsset('previous')} />
@@ -485,15 +576,7 @@
- {#if viewerKind === 'StackPhotoViewer'} - navigateAsset('previous')} - onNextAsset={() => navigateAsset('next')} - haveFadeTransition={false} - {sharedLink} - /> - {:else if viewerKind === 'StackVideoViewer'} + {#if viewerKind === 'StackVideoViewer'} {:else if viewerKind === 'PhotoViewer'} - navigateAsset('previous')} - onNextAsset={() => navigateAsset('next')} - {sharedLink} - haveFadeTransition={$slideshowState !== SlideshowState.None && $slideshowTransition} - /> + {:else if viewerKind === 'VideoViewer'} - {#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && nextAsset} + {#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && !isFaceEditMode.value && nextAsset}
navigateAsset('next')} />
@@ -577,7 +654,7 @@ class="row-start-1 row-span-4 w-90 overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray bg-light" translate="yes" > - +
{/if} @@ -595,10 +672,14 @@ {#if stack && withStacked && !assetViewerManager.isShowEditor} {@const stackedAssets = stack.assets}
-
+