diff --git a/e2e/src/specs/web/photo-viewer.e2e-spec.ts b/e2e/src/specs/web/photo-viewer.e2e-spec.ts index 3f9bb4237a..0918309596 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/e2e/src/ui/specs/timeline/utils.ts b/e2e/src/ui/specs/timeline/utils.ts index 774839b174..e3799a7c3b 100644 --- a/e2e/src/ui/specs/timeline/utils.ts +++ b/e2e/src/ui/specs/timeline/utils.ts @@ -65,7 +65,7 @@ export const thumbnailUtils = { return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"] button`); }, selectedAsset(page: Page) { - return page.locator('[data-thumbnail-focus-container]:has(button[aria-checked])'); + return page.locator('[data-thumbnail-focus-container][data-selected]'); }, async clickAssetId(page: Page, assetId: string) { await thumbnailUtils.withAssetId(page, assetId).click(); @@ -103,11 +103,8 @@ export const thumbnailUtils = { await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0); }, async expectSelectedReadonly(page: Page, assetId: string) { - // todo - need a data attribute for selected await expect( - page.locator( - `[data-thumbnail-focus-container][data-asset="${assetId}"] > .group.cursor-not-allowed > .rounded-xl`, - ), + page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"][data-selected]`), ).toBeVisible(); }, async expectTimelineHasOnScreenAssets(page: Page) { diff --git a/e2e/src/web/specs/photo-viewer.e2e-spec.ts b/e2e/src/web/specs/photo-viewer.e2e-spec.ts deleted file mode 100644 index 0918309596..0000000000 --- a/e2e/src/web/specs/photo-viewer.e2e-spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk'; -import { expect, test } from '@playwright/test'; -import { utils } from 'src/utils'; - -test.describe('Photo Viewer', () => { - let admin: LoginResponseDto; - let asset: AssetMediaResponseDto; - let rawAsset: AssetMediaResponseDto; - - test.beforeAll(async () => { - utils.initSdk(); - await utils.resetDatabase(); - admin = await utils.adminSetup(); - asset = await utils.createAsset(admin.accessToken); - rawAsset = await utils.createAsset(admin.accessToken, { assetData: { filename: 'test.arw' } }); - }); - - test.beforeEach(async ({ context, page }) => { - // before each test, login as user - await utils.setAuthCookies(context, admin.accessToken); - await page.waitForLoadState('networkidle'); - }); - - test('loads original photo when zoomed', async ({ page }) => { - await page.goto(`/photos/${asset.id}`); - 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(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(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(page.getByTestId('original')).toHaveAttribute('src', /fullsize/); - }); - - test('reloads photo when checksum changes', async ({ page }) => { - await page.goto(`/photos/${asset.id}`); - 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(page.getByTestId('preview')).not.toHaveAttribute('src', initialSrc!); - }); -}); diff --git a/e2e/src/web/specs/timeline/utils.ts b/e2e/src/web/specs/timeline/utils.ts deleted file mode 100644 index 6cd44cd784..0000000000 --- a/e2e/src/web/specs/timeline/utils.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { BrowserContext, expect, Page } from '@playwright/test'; -import { DateTime } from 'luxon'; -import { TimelineAssetConfig } from 'src/generators/timeline'; - -export const sleep = (ms: number) => { - return new Promise((resolve) => setTimeout(resolve, ms)); -}; - -export const padYearMonth = (yearMonth: string) => { - const [year, month] = yearMonth.split('-'); - return `${year}-${month.padStart(2, '0')}`; -}; - -export async function throttlePage(context: BrowserContext, page: Page) { - const session = await context.newCDPSession(page); - await session.send('Network.emulateNetworkConditions', { - offline: false, - downloadThroughput: (1.5 * 1024 * 1024) / 8, - uploadThroughput: (750 * 1024) / 8, - latency: 40, - connectionType: 'cellular3g', - }); - await session.send('Emulation.setCPUThrottlingRate', { rate: 10 }); -} - -export const poll = async ( - page: Page, - query: () => Promise, - callback?: (result: Awaited | undefined) => boolean, -) => { - let result; - const timeout = Date.now() + 10_000; - - const terminate = callback || ((result: Awaited | undefined) => !!result); - while (!terminate(result) && Date.now() < timeout) { - try { - result = await query(); - } catch { - // ignore - } - if (page.isClosed()) { - return; - } - try { - await page.waitForTimeout(50); - } catch { - return; - } - } - if (!result) { - // rerun to trigger error if any - result = await query(); - } - return result; -}; - -export const thumbnailUtils = { - locator(page: Page) { - return page.locator('[data-thumbnail-focus-container]'); - }, - withAssetId(page: Page, assetId: string) { - return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"]`); - }, - selectButton(page: Page, assetId: string) { - return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"] button`); - }, - selectedAsset(page: Page) { - return page.locator('[data-thumbnail-focus-container][data-selected]'); - }, - async clickAssetId(page: Page, assetId: string) { - await thumbnailUtils.withAssetId(page, assetId).click(); - }, - async queryThumbnailInViewport(page: Page, collector: (assetId: string) => boolean) { - const assetIds: string[] = []; - for (const thumb of await this.locator(page).all()) { - const box = await thumb.boundingBox(); - if (box) { - const assetId = await thumb.evaluate((e) => e.dataset.asset); - if (collector?.(assetId!)) { - return [assetId!]; - } - assetIds.push(assetId!); - } - } - return assetIds; - }, - async getFirstInViewport(page: Page) { - return await poll(page, () => thumbnailUtils.queryThumbnailInViewport(page, () => true)); - }, - async getAllInViewport(page: Page, collector: (assetId: string) => boolean) { - return await poll(page, () => thumbnailUtils.queryThumbnailInViewport(page, collector)); - }, - async expectThumbnailIsFavorite(page: Page, assetId: string) { - await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-favorite]')).toHaveCount(1); - }, - async expectThumbnailIsNotFavorite(page: Page, assetId: string) { - await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-favorite]')).toHaveCount(0); - }, - async expectThumbnailIsArchive(page: Page, assetId: string) { - await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(1); - }, - async expectThumbnailIsNotArchive(page: Page, assetId: string) { - await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0); - }, - async expectSelectedReadonly(page: Page, assetId: string) { - await expect( - page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"][data-selected]`), - ).toBeVisible(); - }, - async expectTimelineHasOnScreenAssets(page: Page) { - const first = await thumbnailUtils.getFirstInViewport(page); - if (page.isClosed()) { - return; - } - expect(first).toBeTruthy(); - }, - async expectInViewport(page: Page, assetId: string) { - const box = await poll(page, () => thumbnailUtils.withAssetId(page, assetId).boundingBox()); - if (page.isClosed()) { - return; - } - expect(box).toBeTruthy(); - }, - async expectBottomIsTimelineBottom(page: Page, assetId: string) { - const box = await thumbnailUtils.withAssetId(page, assetId).boundingBox(); - const gridBox = await timelineUtils.locator(page).boundingBox(); - if (page.isClosed()) { - return; - } - expect(box!.y + box!.height).toBeCloseTo(gridBox!.y + gridBox!.height, 0); - }, - async expectTopIsTimelineTop(page: Page, assetId: string) { - const box = await thumbnailUtils.withAssetId(page, assetId).boundingBox(); - const gridBox = await timelineUtils.locator(page).boundingBox(); - if (page.isClosed()) { - return; - } - expect(box!.y).toBeCloseTo(gridBox!.y, 0); - }, -}; -export const timelineUtils = { - locator(page: Page) { - return page.locator('#asset-grid'); - }, - async waitForTimelineLoad(page: Page) { - await expect(timelineUtils.locator(page)).toBeInViewport(); - await expect.poll(() => thumbnailUtils.locator(page).count()).toBeGreaterThan(0); - }, - async getScrollTop(page: Page) { - const queryTop = () => - page.evaluate(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return document.querySelector('#asset-grid').scrollTop; - }); - await expect.poll(queryTop).toBeGreaterThan(0); - return await queryTop(); - }, -}; - -export const assetViewerUtils = { - locator(page: Page) { - return page.locator('#immich-asset-viewer'); - }, - async waitForViewerLoad(page: Page, asset: TimelineAssetConfig) { - await page - .locator( - `img[draggable="false"][src="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}&edited=true"]`, - ) - .or( - page.locator(`video[poster="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}&edited=true"]`), - ) - .waitFor(); - }, - async expectActiveAssetToBe(page: Page, assetId: string) { - const activeElement = () => - page.evaluate(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return document.activeElement?.dataset?.asset; - }); - await expect(poll(page, activeElement, (result) => result === assetId)).resolves.toBe(assetId); - }, -}; -export const pageUtils = { - async deepLinkPhotosPage(page: Page, assetId: string) { - await page.goto(`/photos?at=${assetId}`); - await timelineUtils.waitForTimelineLoad(page); - }, - async openPhotosPage(page: Page) { - await page.goto(`/photos`); - await timelineUtils.waitForTimelineLoad(page); - }, - async openFavorites(page: Page) { - await page.goto(`/favorites`); - await timelineUtils.waitForTimelineLoad(page); - }, - async openAlbumPage(page: Page, albumId: string) { - await page.goto(`/albums/${albumId}`); - await timelineUtils.waitForTimelineLoad(page); - }, - async openArchivePage(page: Page) { - await page.goto(`/archive`); - await timelineUtils.waitForTimelineLoad(page); - }, - async deepLinkAlbumPage(page: Page, albumId: string, assetId: string) { - await page.goto(`/albums/${albumId}?at=${assetId}`); - await timelineUtils.waitForTimelineLoad(page); - }, - async goToAsset(page: Page, assetDate: string) { - await timelineUtils.locator(page).hover(); - const stringDate = DateTime.fromISO(assetDate).toFormat('MMddyyyy,hh:mm:ss.SSSa'); - await page.keyboard.press('g'); - await page.locator('#datetime').pressSequentially(stringDate); - await page.getByText('Confirm').click(); - }, - async selectDay(page: Page, day: string) { - await page.getByTitle(day).hover(); - await page.locator('[data-group] .w-8').click(); - }, - async pauseTestDebug() { - console.log('NOTE: pausing test indefinately for debug'); - await new Promise(() => void 0); - }, -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9af490b82a..46f0db0aac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2887,105 +2887,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -3843,42 +3827,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -4168,79 +4146,66 @@ packages: resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.55.1': resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.55.1': resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.55.1': resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.55.1': resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.55.1': resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.55.1': resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.55.1': resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.55.1': resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.55.1': resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.55.1': resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.55.1': resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.55.1': resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.55.1': resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} @@ -4471,28 +4436,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.15.11': resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-x64-gnu@1.15.11': resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.15.11': resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.15.11': resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==} @@ -4572,28 +4533,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -8451,28 +8408,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} diff --git a/web/src/lib/actions/image-loader.svelte.ts b/web/src/lib/actions/image-loader.svelte.ts index 10b642065c..1de0566842 100644 --- a/web/src/lib/actions/image-loader.svelte.ts +++ b/web/src/lib/actions/image-loader.svelte.ts @@ -1,77 +1,9 @@ import { cancelImageUrl } from '$lib/utils/sw-messaging'; -import type { ClassValue } from 'svelte/elements'; -/** - * Converts a ClassValue to a string suitable for className assignment. - * Handles strings, arrays, and objects similar to how clsx works. - */ -function classValueToString(value: ClassValue | undefined): string { - if (!value) { - return ''; - } - if (typeof value === 'string') { - return value; - } - if (Array.isArray(value)) { - return value - .map((v) => classValueToString(v)) - .filter(Boolean) - .join(' '); - } - // Object/dictionary case - return Object.entries(value) - .filter(([, v]) => v) - .map(([k]) => k) - .join(' '); -} - -export type ImageLoaderProperties = { - imgClass?: ClassValue; +type ImageLoaderProperties = { alt?: string; draggable?: boolean; - role?: string; - style?: string; - title?: string | null; loading?: 'lazy' | 'eager'; - dataAttributes?: Record; -}; -export type ImageSourceProperty = { - src: string | undefined; -}; -export type ImageLoaderCallbacks = { - onStart?: () => void; - onLoad?: () => void; - onError?: (error: Error) => void; - onElementCreated?: (element: HTMLImageElement) => void; -}; - -const updateImageAttributes = (img: HTMLImageElement, params: ImageLoaderProperties) => { - if (params.alt !== undefined) { - img.alt = params.alt; - } - if (params.draggable !== undefined) { - img.draggable = params.draggable; - } - if (params.imgClass) { - img.className = classValueToString(params.imgClass); - } - if (params.role) { - img.role = params.role; - } - if (params.style !== undefined) { - img.setAttribute('style', params.style); - } - if (params.title !== undefined && params.title !== null) { - img.title = params.title; - } - if (params.loading !== undefined) { - img.loading = params.loading; - } - if (params.dataAttributes) { - for (const [key, value] of Object.entries(params.dataAttributes)) { - img.setAttribute(key, value); - } - } }; const destroyImageElement = ( @@ -92,23 +24,27 @@ const createImageElement = ( onLoad: () => void, onError: () => void, onStart?: () => void, - onElementCreated?: (imgElement: HTMLImageElement) => void, ) => { if (!src) { return undefined; } const img = document.createElement('img'); - updateImageAttributes(img, properties); + + if (properties.alt !== undefined) { + img.alt = properties.alt; + } + if (properties.draggable !== undefined) { + img.draggable = properties.draggable; + } + if (properties.loading !== undefined) { + img.loading = properties.loading; + } img.addEventListener('load', onLoad); img.addEventListener('error', onError); onStart?.(); - - if (src) { - img.src = src; - onElementCreated?.(img); - } + img.src = src; return img; }; @@ -141,67 +77,3 @@ export function loadImage( } export type LoadImageFunction = typeof loadImage; - -/** - * 1. Creates and appends an element to the parent - * 2. Coordinates with service worker before src triggers fetch - * 3. Adds load/error listeners - * 4. Cancels SW request when element is removed from DOM - */ -export function imageLoader( - node: HTMLElement, - params: ImageSourceProperty & ImageLoaderProperties & ImageLoaderCallbacks, -) { - let currentSrc = params.src; - let currentCallbacks = params; - let imgElement: HTMLImageElement | undefined = undefined; - - const handleLoad = () => { - currentCallbacks.onLoad?.(); - }; - - const handleError = () => { - currentCallbacks.onError?.(new Error(`Failed to load image: ${currentSrc}`)); - }; - - const handleElementCreated = (img: HTMLImageElement) => { - if (img) { - node.append(img); - currentCallbacks.onElementCreated?.(img); - } - }; - - const createImage = () => { - imgElement = createImageElement(currentSrc, params, handleLoad, handleError, params.onStart, handleElementCreated); - }; - createImage(); - - return { - update(newParams: ImageSourceProperty & ImageLoaderProperties & ImageLoaderCallbacks) { - // If src changed, recreate the image element - if (newParams.src !== currentSrc) { - if (imgElement) { - destroyImageElement(imgElement, currentSrc, handleLoad, handleError); - } - currentSrc = newParams.src; - currentCallbacks = newParams; - - createImage(); - return; - } - - currentCallbacks = newParams; - - if (!imgElement) { - return; - } - updateImageAttributes(imgElement, newParams); - }, - - destroy() { - if (imgElement) { - destroyImageElement(imgElement, currentSrc, handleLoad, handleError); - } - }, - }; -} diff --git a/web/src/lib/components/asset-viewer/adaptive-image.svelte b/web/src/lib/components/AdaptiveImage.svelte similarity index 57% rename from web/src/lib/components/asset-viewer/adaptive-image.svelte rename to web/src/lib/components/AdaptiveImage.svelte index 2fe919c7f8..e4f7e81fa8 100644 --- a/web/src/lib/components/asset-viewer/adaptive-image.svelte +++ b/web/src/lib/components/AdaptiveImage.svelte @@ -1,8 +1,8 @@ -
+
{#if blurredSlideshow} - {#if asset.thumbhash} - - - {:else if showSpinner} -
- -
+ {#if showAlphaBackground} + {/if} -
adaptiveImageLoader.onThumbnailStart(), - onLoad: () => adaptiveImageLoader.onThumbnailLoad(), - onError: () => adaptiveImageLoader.onThumbnailError(), - onElementCreated: (element) => (thumbnailElement = element), - imgClass: ['absolute h-full', 'w-full'], - alt: '', - role: 'presentation', - dataAttributes: { - 'data-testid': 'thumbnail', - }, - }} - >
+ {#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} - {:else} -
adaptiveImageLoader.onPreviewStart(), - onLoad: () => adaptiveImageLoader.onPreviewLoad(), - onError: () => adaptiveImageLoader.onPreviewError(), - onElementCreated: (element) => (previewElement = element), - imgClass: ['h-full', 'w-full', imageClass], - alt: imageAltText, - draggable: false, - dataAttributes: { - 'data-testid': 'preview', - }, - }} - > - {@render overlays?.()} -
+ {/if} -
adaptiveImageLoader.onOriginalStart(), - onLoad: () => adaptiveImageLoader.onOriginalLoad(), - onError: () => adaptiveImageLoader.onOriginalError(), - onElementCreated: (element) => (originalElement = element), - imgClass: ['h-full', 'w-full', imageClass], - alt: imageAltText, - draggable: false, - dataAttributes: { - 'data-testid': 'original', - }, - }} - > - {@render overlays?.()} -
+ {#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}
@@ -219,12 +236,9 @@ visibility: visible; } } + #spinner { visibility: hidden; animation: 0s linear 0.4s forwards delayedVisibility; } - :global(.checkerboard) { - background-image: conic-gradient(#808080 25%, #b0b0b0 25% 50%, #808080 50% 75%, #b0b0b0 75%); - background-size: 20px 20px; - } diff --git a/web/src/lib/components/AlphaBackground.svelte b/web/src/lib/components/AlphaBackground.svelte new file mode 100644 index 0000000000..c0d8536a2f --- /dev/null +++ b/web/src/lib/components/AlphaBackground.svelte @@ -0,0 +1,11 @@ + + +
diff --git a/web/src/lib/components/Image.svelte b/web/src/lib/components/Image.svelte new file mode 100644 index 0000000000..2b50b379fe --- /dev/null +++ b/web/src/lib/components/Image.svelte @@ -0,0 +1,49 @@ + + +{#if src} + {#key src} + + {/key} +{/if} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index eb46673d49..8282d8ca31 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -222,8 +222,8 @@ const movedForward = newCursor.current.id === oldCursor.nextAsset?.id; const movedBackward = newCursor.current.id === oldCursor.previousAsset?.id; - const shouldDestroyPrevious = movedForward || !movedBackward; - const shouldDestroyNext = movedBackward || !movedForward; + const shouldDestroyPrevious = !movedBackward; + const shouldDestroyNext = !movedForward; if (shouldDestroyPrevious) { destroyPreviousPreloader(); @@ -438,7 +438,7 @@ // After navigation completes, reconcile preloads with full state information updatePreloadsAfterNavigation(lastCursor, cursor); } - if (!lastCursor && cursor) { + if (!lastCursor) { // "first time" load, start preloads if (cursor.nextAsset) { nextPreloader = startPreloader(cursor.nextAsset); diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 1df6662c06..ea2f653d7d 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -7,7 +7,6 @@ import { timeToLoadTheMap } from '$lib/constants'; import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; - import { eventManager } from '$lib/managers/event-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import AssetChangeDateModal from '$lib/modals/AssetChangeDateModal.svelte'; import { Route } from '$lib/route'; @@ -42,11 +41,12 @@ mdiPlus, } from '@mdi/js'; import { DateTime } from 'luxon'; - import { onMount, untrack } from 'svelte'; + import { untrack } from 'svelte'; import { t } from 'svelte-i18n'; import { slide } from 'svelte/transition'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import PersonSidePanel from '../faces-page/person-side-panel.svelte'; + import OnEvents from '../OnEvents.svelte'; import UserAvatar from '../shared-components/user-avatar.svelte'; import AlbumListItemDetails from './album-list-item-details.svelte'; @@ -96,8 +96,6 @@ } }; - onMount(() => eventManager.on({ AlbumAddAssets: () => void refreshAlbums() })); - $effect(() => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions asset; @@ -153,6 +151,8 @@ }; + void refreshAlbums()} /> +
import { shortcuts } from '$lib/actions/shortcut'; - import AdaptiveImage from '$lib/components/asset-viewer/adaptive-image.svelte'; + import { zoomImageAction } from '$lib/actions/zoom-image'; + import AdaptiveImage from '$lib/components/AdaptiveImage.svelte'; import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte'; import OcrBoundingBox from '$lib/components/asset-viewer/ocr-bounding-box.svelte'; import AssetViewerEvents from '$lib/components/AssetViewerEvents.svelte'; @@ -109,6 +110,7 @@ width: containerWidth, height: containerHeight, }); + let adaptiveImage = $state(); @@ -127,13 +129,13 @@ class="relative h-full w-full select-none" bind:clientWidth={containerWidth} bind:clientHeight={containerHeight} + use:zoomImageAction={{ disabled: isOcrActive, zoomTarget: adaptiveImage }} > onReady?.()} @@ -141,7 +143,8 @@ onError?.(); onReady?.(); }} - bind:imgElement={assetViewerManager.imgRef} + bind:imgRef={assetViewerManager.imgRef} + bind:ref={adaptiveImage} > {#snippet overlays()} diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index 00ba3addb7..572e2a4e75 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -1,6 +1,7 @@
= { + basic: 0, + 'loading-thumbnail': 1, + thumbnail: 2, + 'loading-preview': 3, + preview: 4, + 'loading-original': 5, + original: 6, +}; + export interface ImageLoaderState { previewUrl?: string; thumbnailUrl?: string; @@ -25,6 +35,7 @@ export interface ImageLoaderState { previewImage: ImageStatus; originalImage: ImageStatus; } + enum ImageStatus { Unloaded = 'Unloaded', Success = 'Success', @@ -36,7 +47,11 @@ enum ImageStatus { * thumbhash → thumbnail → preview → original (on zoom) * */ +let nextLoaderId = 0; + export class AdaptiveImageLoader { + readonly id = nextLoaderId++; + private internalState = $state({ quality: 'basic', hasError: false, @@ -98,10 +113,19 @@ export class AdaptiveImageLoader { return this.internalState; } + private shouldUpdateQuality(newQuality: ImageQuality): boolean { + const currentLevel = qualityOrder[this.internalState.quality]; + const newLevel = qualityOrder[newQuality]; + return newLevel > currentLevel; + } + onThumbnailStart() { if (this.destroyed) { return; } + if (!this.shouldUpdateQuality('loading-thumbnail')) { + return; + } this.internalState.quality = 'loading-thumbnail'; } @@ -109,6 +133,9 @@ export class AdaptiveImageLoader { if (this.destroyed) { return; } + if (!this.shouldUpdateQuality('thumbnail')) { + return; + } this.internalState.quality = 'thumbnail'; this.internalState.thumbnailImage = ImageStatus.Success; this.callbacks?.onImageReady?.(); @@ -137,6 +164,10 @@ export class AdaptiveImageLoader { this.triggerOriginal(); return false; } + if (this.internalState.previewUrl) { + // Already triggered + return true; + } this.internalState.hasError = false; this.internalState.previewUrl = this.previewUrl; if (this.imageLoader) { @@ -156,6 +187,9 @@ export class AdaptiveImageLoader { if (this.destroyed) { return; } + if (!this.shouldUpdateQuality('loading-preview')) { + return; + } this.internalState.quality = 'loading-preview'; } @@ -163,6 +197,12 @@ export class AdaptiveImageLoader { if (this.destroyed) { return; } + if (!this.internalState.previewUrl) { + return; + } + if (!this.shouldUpdateQuality('preview')) { + return; + } this.internalState.quality = 'preview'; this.internalState.previewImage = ImageStatus.Success; this.callbacks?.onImageReady?.(); @@ -172,7 +212,6 @@ export class AdaptiveImageLoader { if (this.destroyed || imageManager.isCanceled(this.asset)) { return; } - this.internalState.hasError = true; this.internalState.previewImage = ImageStatus.Error; this.internalState.previewUrl = undefined; @@ -184,8 +223,11 @@ export class AdaptiveImageLoader { if (!this.originalUrl) { return false; } + if (this.internalState.originalUrl) { + // Already triggered + return true; + } this.internalState.hasError = false; - this.internalState.originalUrl = this.originalUrl; if (this.imageLoader) { @@ -205,6 +247,9 @@ export class AdaptiveImageLoader { if (this.destroyed || imageManager.isCanceled(this.asset)) { return; } + if (!this.shouldUpdateQuality('loading-original')) { + return; + } this.internalState.quality = 'loading-original'; } @@ -212,6 +257,12 @@ export class AdaptiveImageLoader { if (this.destroyed || imageManager.isCanceled(this.asset)) { return; } + if (!this.internalState.originalUrl) { + return; + } + if (!this.shouldUpdateQuality('original')) { + return; + } this.internalState.quality = 'original'; this.internalState.originalImage = ImageStatus.Success; this.callbacks?.onImageReady?.(); @@ -221,7 +272,6 @@ export class AdaptiveImageLoader { if (this.destroyed || imageManager.isCanceled(this.asset)) { return; } - this.internalState.hasError = true; this.internalState.originalImage = ImageStatus.Error; this.internalState.originalUrl = undefined;