diff --git a/e2e/src/web/specs/timeline/utils.ts b/e2e/src/web/specs/timeline/utils.ts index 0f04bf9361..6cd44cd784 100644 --- a/e2e/src/web/specs/timeline/utils.ts +++ b/e2e/src/web/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/web/src/lib/components/assets/broken-asset.svelte b/web/src/lib/components/assets/broken-asset.svelte index a15a787e64..16903112d3 100644 --- a/web/src/lib/components/assets/broken-asset.svelte +++ b/web/src/lib/components/assets/broken-asset.svelte @@ -2,24 +2,34 @@ import { Icon } from '@immich/ui'; import { mdiImageBrokenVariant } from '@mdi/js'; import { t } from 'svelte-i18n'; + import type { ClassValue } from 'svelte/elements'; interface Props { - class?: string; + class?: ClassValue; hideMessage?: boolean; width?: string | undefined; height?: string | undefined; } let { class: className = '', hideMessage = false, width = undefined, height = undefined }: Props = $props(); + + let clientWidth = $state(0); + let textClass = $derived(clientWidth < 100 ? 'text-xs' : clientWidth < 150 ? 'text-sm' : 'text-base');
- + {#if clientWidth >= 75} + + {/if} {#if !hideMessage} - {$t('error_loading_image')} + {$t('error_loading_image')} {/if}
diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index 18bfd11058..00ba3addb7 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -48,23 +48,24 @@ loaded = true; onComplete?.(false); }; + const setErrored = () => { errored = true; onComplete?.(true); }; - let optionalClasses = $derived( + let optionalClasses = $derived([ [ curve && 'rounded-xl', circle && 'rounded-full', shadow && 'shadow-lg', (circle || !heightStyle) && 'aspect-square', border && 'border-3 border-immich-dark-primary/80 hover:border-immich-primary', - brokenAssetClass, ] .filter(Boolean) .join(' '), - ); + brokenAssetClass, + ]); let style = $derived( `width: ${widthStyle}; height: ${heightStyle ?? ''}; filter: ${hidden ? 'grayscale(50%)' : 'none'}; opacity: ${hidden ? '0.5' : '1'};`, @@ -79,7 +80,7 @@ src: url, onLoad: setLoaded, onError: setErrored, - imgClass: ['object-cover', optionalClasses, imageClass], + imgClass: ['object-cover', imageClass], style, alt: loaded || errored ? altText : '', draggable: false, diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index 8dfa14fcdc..e750d4bd77 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -200,8 +200,9 @@
- -
-
+ ((loaded = true), (thumbError = errored))} + /> + {#if asset.isVideo} +
+ +
+ {:else if asset.isImage && asset.livePhotoVideoId} +
+ +
+ {:else if asset.isImage && asset.duration && !asset.duration.includes('0:00:00.000') && mouseOver} + +
+
+ +
+ + + +
+
+ {/if} + + {#if (!loaded || thumbError) && asset.thumbhash} + + {/if} +
{#if !usingMobileDevice && !disabled}
@@ -261,7 +322,10 @@ {#if dimmed && !mouseOver} -
+
{/if} @@ -329,72 +393,6 @@ > {/if} - - ((loaded = true), (thumbError = errored))} - /> - {#if asset.isVideo} -
- -
- {:else if asset.isImage && asset.livePhotoVideoId} -
- -
- {:else if asset.isImage && asset.duration && !asset.duration.includes('0:00:00.000') && mouseOver} - -
-
- -
- - - -
-
- {/if} - - {#if (!loaded || thumbError) && asset.thumbhash} - - {/if}
{#if selectionCandidate} @@ -427,11 +425,14 @@ {/if} {/if} + + +
- - diff --git a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte index 222fa7a8ec..6a9df58a61 100644 --- a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte @@ -2,6 +2,7 @@ import { Icon, LoadingSpinner } from '@immich/ui'; import { mdiAlertCircleOutline, mdiPauseCircleOutline, mdiPlayCircleOutline } from '@mdi/js'; import { Duration } from 'luxon'; + import type { ClassValue } from 'svelte/elements'; interface Props { url: string; @@ -12,6 +13,7 @@ curve?: boolean; playIcon?: string; pauseIcon?: string; + class?: ClassValue; } let { @@ -23,6 +25,7 @@ curve = false, playIcon = mdiPlayCircleOutline, pauseIcon = mdiPauseCircleOutline, + class: className, }: Props = $props(); let remainingSeconds = $state(durationInSeconds); @@ -57,7 +60,7 @@ {#if enablePlayback}