diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte
index 566477e743..166d4d0bda 100644
--- a/web/src/lib/components/photos-page/asset-date-group.svelte
+++ b/web/src/lib/components/photos-page/asset-date-group.svelte
@@ -97,7 +97,7 @@
{#each dateGroups as dateGroup, groupIndex (dateGroup.date)}
{@const display =
dateGroup.intersecting || !!dateGroup.assets.some((asset) => asset.id === $assetStore.pendingScrollAssetId)}
- {@const geometry = dateGroup.geometry}
+ {@const geometry = dateGroup.geometry!}
$assetStore.updateBucketDateGroup(bucket, dateGroup, { height })}>
{dateGroup.groupTitle}
@@ -81,8 +81,8 @@
diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
index df54295f04..d599d6bd37 100644
--- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
+++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
@@ -8,7 +8,7 @@
import type { Viewport } from '$lib/stores/assets.store';
import { showDeleteModal } from '$lib/stores/preferences.store';
import { deleteAssets } from '$lib/utils/actions';
- import { archiveAssets, cancelMultiselect, getJustifiedLayoutFromAssets } from '$lib/utils/asset-utils';
+ import { archiveAssets, cancelMultiselect } from '$lib/utils/asset-utils';
import { featureFlags } from '$lib/stores/server-config.store';
import { handleError } from '$lib/utils/handle-error';
import { navigate } from '$lib/utils/navigation';
@@ -307,14 +307,15 @@
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id));
- let geometry = $derived(
- getJustifiedLayoutFromAssets(assets, {
+ let geometry = $derived.by(async () => {
+ const { getJustifiedLayoutFromAssets } = await import('$lib/utils/layout-utils');
+ return getJustifiedLayoutFromAssets(assets, {
spacing: 2,
- rowWidth: Math.floor(viewport.width),
heightTolerance: 0.15,
rowHeight: 235,
- }),
- );
+ rowWidth: Math.floor(viewport.width),
+ });
+ });
$effect(() => {
if (!lastAssetMouseEvent) {
@@ -350,47 +351,49 @@
{/if}
{#if assets.length > 0}
-
- {#each assets as asset, i}
- {@const top = geometry.getTop(i)}
- {@const left = geometry.getLeft(i)}
- {@const width = geometry.getWidth(i)}
- {@const height = geometry.getHeight(i)}
+ {#await geometry then geometry}
+
+ {#each assets as asset, i}
+ {@const top = geometry.getTop(i)}
+ {@const left = geometry.getLeft(i)}
+ {@const width = geometry.getWidth(i)}
+ {@const height = geometry.getHeight(i)}
-
-
{
- if (assetInteraction.selectionActive) {
- handleSelectAssets(asset);
- return;
- }
- void viewAssetHandler(asset);
- }}
- onSelect={(asset) => handleSelectAssets(asset)}
- onMouseEvent={() => assetMouseEventHandler(asset)}
- onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)}
- {showArchiveIcon}
- {asset}
- selected={assetInteraction.selectedAssets.has(asset)}
- selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)}
- thumbnailWidth={width}
- thumbnailHeight={height}
- />
- {#if showAssetName}
-
- {asset.originalFileName}
-
- {/if}
-
- {/each}
-
+
+
{
+ if (assetInteraction.selectionActive) {
+ handleSelectAssets(asset);
+ return;
+ }
+ void viewAssetHandler(asset);
+ }}
+ onSelect={(asset) => handleSelectAssets(asset)}
+ onMouseEvent={() => assetMouseEventHandler(asset)}
+ onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)}
+ {showArchiveIcon}
+ {asset}
+ selected={assetInteraction.selectedAssets.has(asset)}
+ selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)}
+ thumbnailWidth={width}
+ thumbnailHeight={height}
+ />
+ {#if showAssetName}
+
+ {asset.originalFileName}
+
+ {/if}
+
+ {/each}
+
+ {/await}
{/if}
diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts
index 8e02562e85..2eaf5d36e6 100644
--- a/web/src/lib/stores/assets.store.ts
+++ b/web/src/lib/stores/assets.store.ts
@@ -1,7 +1,6 @@
import { locale } from '$lib/stores/preferences.store';
import { getKey } from '$lib/utils';
import { AssetGridTaskManager } from '$lib/utils/asset-store-task-manager';
-import { getJustifiedLayoutFromAssets } from '$lib/utils/asset-utils';
import { generateId } from '$lib/utils/generate-id';
import type { AssetGridRouteSearchParams } from '$lib/utils/navigation';
import { fromLocalDateTime, splitBucketIntoDateGroups, type DateGroup } from '$lib/utils/timeline-util';
@@ -436,7 +435,7 @@ export class AssetStore {
private async initialLayout(changedWidth: boolean) {
for (const bucket of this.buckets) {
- this.updateGeometry(bucket, changedWidth);
+ await this.updateGeometry(bucket, changedWidth);
}
this.timelineHeight = this.buckets.reduce((accumulator, b) => accumulator + b.bucketHeight, 0);
@@ -454,7 +453,7 @@ export class AssetStore {
this.emit(false);
}
- private updateGeometry(bucket: AssetBucket, invalidateHeight: boolean) {
+ private async updateGeometry(bucket: AssetBucket, invalidateHeight: boolean) {
if (invalidateHeight) {
bucket.isBucketHeightActual = false;
bucket.measured = false;
@@ -477,6 +476,8 @@ export class AssetStore {
rowHeight: 235,
rowWidth: Math.floor(viewportWidth),
};
+ // TODO: move this import and make this method sync after https://github.com/sveltejs/kit/issues/7805 is fixed
+ const { getJustifiedLayoutFromAssets } = await import('$lib/utils/layout-utils');
for (const assetGroup of bucket.dateGroups) {
if (!assetGroup.heightActual) {
const unwrappedWidth = (3 / 2) * assetGroup.assets.length * THUMBNAIL_HEIGHT * (7 / 10);
@@ -552,7 +553,7 @@ export class AssetStore {
bucket.assets = assets;
bucket.dateGroups = splitBucketIntoDateGroups(bucket, get(locale));
this.maxBucketAssets = Math.max(this.maxBucketAssets, assets.length);
- this.updateGeometry(bucket, true);
+ await this.updateGeometry(bucket, true);
this.timelineHeight = this.buckets.reduce((accumulator, b) => accumulator + b.bucketHeight, 0);
bucket.loaded();
this.notifyListeners({ type: 'loaded', bucket });
@@ -679,7 +680,7 @@ export class AssetStore {
return bDate.diff(aDate).milliseconds;
});
bucket.dateGroups = splitBucketIntoDateGroups(bucket, get(locale));
- this.updateGeometry(bucket, true);
+ void this.updateGeometry(bucket, true);
}
this.emit(true);
@@ -821,7 +822,7 @@ export class AssetStore {
}
if (changed) {
bucket.dateGroups = splitBucketIntoDateGroups(bucket, get(locale));
- this.updateGeometry(bucket, true);
+ void this.updateGeometry(bucket, true);
}
}
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index d3090bf066..70f5c5f8f2 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -12,7 +12,6 @@ import { downloadRequest, getKey, withError } from '$lib/utils';
import { createAlbum } from '$lib/utils/album-utils';
import { getByteUnitString } from '$lib/utils/byte-units';
import { getFormatter } from '$lib/utils/i18n';
-import { JustifiedLayout, type LayoutOptions } from '@immich/justified-layout-wasm';
import {
addAssetsToAlbum as addAssets,
createStack,
@@ -588,13 +587,3 @@ export const copyImageToClipboard = async (source: HTMLImageElement | string) =>
const blob = source instanceof HTMLImageElement ? await imgToBlob(source) : await urlToBlob(source);
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
};
-
-export function getJustifiedLayoutFromAssets(assets: AssetResponseDto[], options: LayoutOptions) {
- const aspectRatios = new Float32Array(assets.length);
- // eslint-disable-next-line unicorn/no-for-loop
- for (let i = 0; i < assets.length; i++) {
- const { width, height } = getAssetRatio(assets[i]);
- aspectRatios[i] = width / height;
- }
- return new JustifiedLayout(aspectRatios, options);
-}
diff --git a/web/src/lib/utils/layout-utils.ts b/web/src/lib/utils/layout-utils.ts
new file mode 100644
index 0000000000..c856c90a4f
--- /dev/null
+++ b/web/src/lib/utils/layout-utils.ts
@@ -0,0 +1,14 @@
+import { getAssetRatio } from '$lib/utils/asset-utils';
+// note: it's important that this is not imported in more than one file due to https://github.com/sveltejs/kit/issues/7805
+import { JustifiedLayout, type LayoutOptions } from '@immich/justified-layout-wasm';
+import type { AssetResponseDto } from '@immich/sdk';
+
+export function getJustifiedLayoutFromAssets(assets: AssetResponseDto[], options: LayoutOptions) {
+ const aspectRatios = new Float32Array(assets.length);
+ // eslint-disable-next-line unicorn/no-for-loop
+ for (let i = 0; i < assets.length; i++) {
+ const { width, height } = getAssetRatio(assets[i]);
+ aspectRatios[i] = width / height;
+ }
+ return new JustifiedLayout(aspectRatios, options);
+}
diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts
index 44baffb135..51d28431d2 100644
--- a/web/src/lib/utils/timeline-util.ts
+++ b/web/src/lib/utils/timeline-util.ts
@@ -1,6 +1,6 @@
import type { AssetBucket } from '$lib/stores/assets.store';
import { locale } from '$lib/stores/preferences.store';
-import { JustifiedLayout } from '@immich/justified-layout-wasm';
+import type { JustifiedLayout } from '@immich/justified-layout-wasm';
import type { AssetResponseDto } from '@immich/sdk';
import { groupBy, memoize, sortBy } from 'lodash-es';
import { DateTime } from 'luxon';
@@ -13,7 +13,7 @@ export type DateGroup = {
height: number;
heightActual: boolean;
intersecting: boolean;
- geometry: JustifiedLayout;
+ geometry: JustifiedLayout | null;
bucket: AssetBucket;
};
export type ScrubberListener = (
@@ -80,13 +80,6 @@ export function formatGroupTitle(_date: DateTime): string {
return date.toLocaleString(groupDateFormat);
}
-const emptyGeometry = new JustifiedLayout(Float32Array.from([]), {
- rowHeight: 1,
- heightTolerance: 0,
- rowWidth: 1,
- spacing: 0,
-});
-
const formatDateGroupTitle = memoize(formatGroupTitle);
export function splitBucketIntoDateGroups(bucket: AssetBucket, locale: string | undefined): DateGroup[] {
@@ -104,7 +97,7 @@ export function splitBucketIntoDateGroups(bucket: AssetBucket, locale: string |
height: 0,
heightActual: false,
intersecting: false,
- geometry: emptyGeometry,
+ geometry: null,
bucket,
};
});