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, }; });