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}