diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 02544e3e07..8b5b2bff8b 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -4,7 +4,7 @@ import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk'; - import { AssetStore } from '$lib/stores/assets.store'; + import { AssetStore } from '$lib/stores/assets-store.svelte'; import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import DownloadAction from '../photos-page/actions/download-action.svelte'; diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 24701685ef..70467ccb82 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -19,7 +19,7 @@ import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { handleError } from '$lib/utils/handle-error'; import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte'; - import { photoViewerImgElement } from '$lib/stores/assets.store'; + import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; interface Props { diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index f34c141f52..49d6e3dbf4 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -22,7 +22,7 @@ import ImageThumbnail from './image-thumbnail.svelte'; import VideoThumbnail from './video-thumbnail.svelte'; import { currentUrlReplaceAssetId } from '$lib/utils/navigation'; - import { AssetStore } from '$lib/stores/assets.store'; + import { AssetStore } from '$lib/stores/assets-store.svelte'; import type { DateGroup } from '$lib/utils/timeline-util'; diff --git a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte index 9188ab9a4f..09b1d1f0d7 100644 --- a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte @@ -3,7 +3,7 @@ import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import { mdiAlertCircleOutline, mdiPauseCircleOutline, mdiPlayCircleOutline } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; - import { AssetStore } from '$lib/stores/assets.store'; + import { AssetStore } from '$lib/stores/assets-store.svelte'; import { generateId } from '$lib/utils/generate-id'; import { onDestroy } from 'svelte'; diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte index 59bcf6a84c..19a99fdb94 100644 --- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -6,7 +6,7 @@ import { mdiArrowLeftThin, mdiClose, mdiMagnify, mdiPlus } from '@mdi/js'; import { linear } from 'svelte/easing'; import { fly } from 'svelte/transition'; - import { photoViewerImgElement } from '$lib/stores/assets.store'; + import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import SearchPeople from '$lib/components/faces-page/people-search.svelte'; diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index b0e0f8fcc4..73c3ea7ae5 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -25,7 +25,7 @@ import AssignFaceSidePanel from './assign-face-side-panel.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { zoomImageToBase64 } from '$lib/utils/people-utils'; - import { photoViewerImgElement } from '$lib/stores/assets.store'; + import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { t } from 'svelte-i18n'; import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 28ed90ba82..6d0336dabf 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -26,7 +26,7 @@ import { AppRoute, QueryParameter } from '$lib/constants'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import { type Viewport } from '$lib/stores/assets.store'; + import { type Viewport } from '$lib/stores/assets-store.svelte'; import { loadMemories, memoryStore } from '$lib/stores/memory.store'; import { locale, videoViewerMuted } from '$lib/stores/preferences.store'; import { preferences } from '$lib/stores/user.store'; diff --git a/web/src/lib/components/photos-page/actions/archive-action.svelte b/web/src/lib/components/photos-page/actions/archive-action.svelte index 868a5ddd6d..ddc5f3cae4 100644 --- a/web/src/lib/components/photos-page/actions/archive-action.svelte +++ b/web/src/lib/components/photos-page/actions/archive-action.svelte @@ -8,7 +8,7 @@ import { t } from 'svelte-i18n'; interface Props { - onArchive: OnArchive; + onArchive?: OnArchive; menuItem?: boolean; unarchive?: boolean; } @@ -28,7 +28,7 @@ loading = true; const ids = await archiveAssets(assets, isArchived); if (ids) { - onArchive(ids, isArchived); + onArchive?.(ids, isArchived); clearSelect(); } loading = false; diff --git a/web/src/lib/components/photos-page/actions/favorite-action.svelte b/web/src/lib/components/photos-page/actions/favorite-action.svelte index 1bc6764157..c06bbcdc6d 100644 --- a/web/src/lib/components/photos-page/actions/favorite-action.svelte +++ b/web/src/lib/components/photos-page/actions/favorite-action.svelte @@ -13,7 +13,7 @@ import { t } from 'svelte-i18n'; interface Props { - onFavorite: OnFavorite; + onFavorite?: OnFavorite; menuItem?: boolean; removeFavorite: boolean; } @@ -44,7 +44,7 @@ asset.isFavorite = isFavorite; } - onFavorite(ids, isFavorite); + onFavorite?.(ids, isFavorite); notificationController.show({ message: isFavorite diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte index 9e7c2b9163..cc3f75ab56 100644 --- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte +++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte @@ -1,6 +1,6 @@ <script lang="ts"> import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; - import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets.store'; + import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte'; import { mdiSelectAll, mdiSelectRemove } from '@mdi/js'; import { selectAllAssets, cancelMultiselect } from '$lib/utils/asset-utils'; import { t } from 'svelte-i18n'; 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 52704c58ee..94b9a2fdda 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -2,7 +2,7 @@ import { intersectionObserver } from '$lib/actions/intersection-observer'; import Icon from '$lib/components/elements/icon.svelte'; import Skeleton from '$lib/components/photos-page/skeleton.svelte'; - import { AssetBucket, type AssetStore, type Viewport } from '$lib/stores/assets.store'; + import { AssetBucket, type AssetStore, type Viewport } from '$lib/stores/assets-store.svelte'; import { navigate } from '$lib/utils/navigation'; import { findTotalOffset, type DateGroup, type ScrollTargetListener } from '$lib/utils/timeline-util'; import type { AssetResponseDto } from '@immich/sdk'; @@ -89,25 +89,26 @@ }; onDestroy(() => { - $assetStore.taskManager.removeAllTasksForComponent(componentId); + assetStore.taskManager.removeAllTasksForComponent(componentId); }); </script> <section id="asset-group-by-date" class="flex flex-wrap gap-x-12" data-bucket-date={bucketDate} bind:this={element}> {#each dateGroups as dateGroup, groupIndex (dateGroup.date)} {@const display = - dateGroup.intersecting || !!dateGroup.assets.some((asset) => asset.id === $assetStore.pendingScrollAssetId)} + dateGroup.intersecting || !!dateGroup.assets.some((asset) => asset.id === assetStore.pendingScrollAssetId)} + {@const geometry = dateGroup.geometry!} <div id="date-group" use:intersectionObserver={{ onIntersect: () => { - $assetStore.taskManager.intersectedDateGroup(componentId, dateGroup, () => + assetStore.taskManager.intersectedDateGroup(componentId, dateGroup, () => assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: true }), ); }, onSeparate: () => { - $assetStore.taskManager.separatedDateGroup(componentId, dateGroup, () => + assetStore.taskManager.separatedDateGroup(componentId, dateGroup, () => assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: false }), ); }, @@ -118,7 +119,7 @@ data-display={display} data-date-group={dateGroup.date} style:height={dateGroup.height + 'px'} - style:width={dateGroup.geometry.containerWidth + 'px'} + style:width={geometry.containerWidth + 'px'} style:overflow="clip" > {#if !display} @@ -129,7 +130,7 @@ <!-- svelte-ignore a11y-no-static-element-interactions --> <div on:mouseenter={() => - $assetStore.taskManager.queueScrollSensitiveTask({ + assetStore.taskManager.queueScrollSensitiveTask({ componentId, task: () => { isMouseOverGroup = true; @@ -137,7 +138,7 @@ }, })} on:mouseleave={() => { - $assetStore.taskManager.queueScrollSensitiveTask({ + assetStore.taskManager.queueScrollSensitiveTask({ componentId, task: () => { isMouseOverGroup = false; @@ -149,7 +150,7 @@ <!-- Date group title --> <div class="flex z-[100] sticky top-[-1px] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm" - style:width={dateGroup.geometry.containerWidth + 'px'} + style:width={geometry.containerWidth + 'px'} > {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))} <div @@ -174,11 +175,15 @@ <!-- Image grid --> <div class="relative overflow-clip" - style:height={dateGroup.geometry.containerHeight + 'px'} - style:width={dateGroup.geometry.containerWidth + 'px'} + style:height={geometry.containerHeight + 'px'} + style:width={geometry.containerWidth + 'px'} > - {#each dateGroup.assets as asset, index (asset.id)} - {@const box = dateGroup.geometry.boxes[index]} + {#each dateGroup.assets as asset, i (asset.id)} + <!-- getting these together here in this order is very cache-efficient --> + {@const top = geometry.getTop(i)} + {@const left = geometry.getLeft(i)} + {@const width = geometry.getWidth(i)} + {@const height = geometry.getHeight(i)} <!-- update ASSET_GRID_PADDING--> <div use:intersectionObserver={{ @@ -190,10 +195,10 @@ }} data-asset-id={asset.id} class="absolute" - style:width={box.width + 'px'} - style:height={box.height + 'px'} - style:top={box.top + 'px'} - style:left={box.left + 'px'} + style:top={top + 'px'} + style:left={left + 'px'} + style:width={width + 'px'} + style:height={height + 'px'} > <Thumbnail {dateGroup} @@ -203,7 +208,7 @@ bottom: renderThumbsAtBottomMargin, top: renderThumbsAtTopMargin, }} - retrieveElement={$assetStore.pendingScrollAssetId === asset.id} + retrieveElement={assetStore.pendingScrollAssetId === asset.id} onRetrieveElement={(element) => onRetrieveElement(dateGroup, asset, element)} showStackedIcon={withStacked} {showArchiveIcon} @@ -212,11 +217,11 @@ onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)} onSelect={(asset) => assetSelectHandler(asset, dateGroup.assets, dateGroup.groupTitle)} onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, asset)} - selected={assetInteraction.selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} + selected={assetInteraction.selectedAssets.has(asset) || assetStore.albumAssets.has(asset.id)} selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)} - disabled={$assetStore.albumAssets.has(asset.id)} - thumbnailWidth={box.width} - thumbnailHeight={box.height} + disabled={assetStore.albumAssets.has(asset.id)} + thumbnailWidth={width} + thumbnailHeight={height} /> </div> {/each} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index db2c491dbd..ea862cdd1a 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -4,7 +4,7 @@ import type { Action } from '$lib/components/asset-viewer/actions/action'; import { AppRoute, AssetAction } from '$lib/constants'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import { AssetBucket, AssetStore, type BucketListener, type ViewportXY } from '$lib/stores/assets.store'; + import { AssetBucket, AssetStore, type BucketListener, type ViewportXY } from '$lib/stores/assets-store.svelte'; import { locale, showDeleteModal } from '$lib/stores/preferences.store'; import { isSearchEnabled } from '$lib/stores/search.store'; import { featureFlags } from '$lib/stores/server-config.store'; @@ -117,7 +117,6 @@ const isViewportOrigin = () => { return viewport.height === 0 && viewport.width === 0; }; - const isEqual = (a: ViewportXY, b: ViewportXY) => { return a.height == b.height && a.width == b.width && a.x === b.x && a.y === b.y; }; @@ -130,7 +129,7 @@ } if ($gridScrollTarget?.at) { - void $assetStore.scheduleScrollToAssetId($gridScrollTarget, () => { + void assetStore.scheduleScrollToAssetId($gridScrollTarget, () => { element?.scrollTo({ top: 0 }); showSkeleton = false; }); @@ -166,7 +165,7 @@ if (assetGridUpdate) { setTimeout(() => { - void $assetStore.updateViewport(safeViewport, true); + void assetStore.updateViewport(safeViewport, true); const asset = $page.url.searchParams.get('at'); if (asset) { $gridScrollTarget = { at: asset }; @@ -194,31 +193,10 @@ return () => void 0; }; - const _updateLastIntersectedBucketDate = () => { - let elem = document.elementFromPoint(safeViewport.x + 1, safeViewport.y + 1); - - while (elem != null) { - if (elem.id === 'bucket') { - break; - } - elem = elem.parentElement; - } - if (elem) { - lastIntersectedBucketDate = (elem as HTMLElement).dataset.bucketDate; - } - }; - const updateLastIntersectedBucketDate = throttle(_updateLastIntersectedBucketDate, 16, { - leading: false, - trailing: true, - }); - const scrollTolastIntersectedBucket = (adjustedBucket: AssetBucket, delta: number) => { - if (!lastIntersectedBucketDate) { - _updateLastIntersectedBucketDate(); - } if (lastIntersectedBucketDate) { - const currentIndex = $assetStore.buckets.findIndex((b) => b.bucketDate === lastIntersectedBucketDate); - const deltaIndex = $assetStore.buckets.indexOf(adjustedBucket); + const currentIndex = assetStore.buckets.findIndex((b) => b.bucketDate === lastIntersectedBucketDate); + const deltaIndex = assetStore.buckets.indexOf(adjustedBucket); if (deltaIndex < currentIndex) { element?.scrollBy(0, delta); @@ -235,20 +213,23 @@ }; onMount(() => { - void $assetStore + void assetStore .init({ bucketListener }) - .then(() => ($assetStore.connect(), $assetStore.updateViewport(safeViewport))); + .then(() => (assetStore.connect(), assetStore.updateViewport(safeViewport))); if (!enableRouting) { showSkeleton = false; } const dispose = hmrSupport(); return () => { - $assetStore.disconnect(); - $assetStore.destroy(); + assetStore.disconnect(); + assetStore.destroy(); dispose(); }; }); + const _updateViewport = () => void assetStore.updateViewport(safeViewport); + const updateViewport = throttle(_updateViewport, 16); + function getOffset(bucketDate: string) { let offset = 0; for (let a = 0; a < assetStore.buckets.length; a++) { @@ -259,12 +240,10 @@ } return offset; } - const _updateViewport = () => void $assetStore.updateViewport(safeViewport); - const updateViewport = throttle(_updateViewport, 16); const getMaxScrollPercent = () => - ($assetStore.timelineHeight + bottomSectionHeight + topSectionHeight - safeViewport.height) / - ($assetStore.timelineHeight + bottomSectionHeight + topSectionHeight); + (assetStore.timelineHeight + bottomSectionHeight + topSectionHeight - safeViewport.height) / + (assetStore.timelineHeight + bottomSectionHeight + topSectionHeight); const getMaxScroll = () => { if (!element || !timelineElement) { @@ -292,7 +271,7 @@ scrollPercent: number, bucketScrollPercent: number, ) => { - if (!bucketDate || $assetStore.timelineHeight < safeViewport.height * 2) { + if (!bucketDate || assetStore.timelineHeight < safeViewport.height * 2) { // edge case - scroll limited due to size of content, must adjust - use use the overall percent instead const maxScroll = getMaxScroll(); @@ -318,7 +297,7 @@ _scrollPercent: number, bucketScrollPercent: number, ) => { - if (!bucketDate || $assetStore.timelineHeight < safeViewport.height * 2) { + if (!bucketDate || assetStore.timelineHeight < safeViewport.height * 2) { // edge case - scroll limited due to size of content, must adjust - use use the overall percent instead return; } @@ -328,10 +307,7 @@ } if (bucket && !bucket.measured) { preMeasure.push(bucket); - if (!bucket.loaded) { - await assetStore.loadBucket(bucket.bucketDate); - } - // Wait here, and collect the deltas that are above offset, which affect offset position + await assetStore.loadBucket(bucketDate, { preventCancel: true, pending: true }); await bucket.measuredPromise; scrollToBucketAndOffset(bucket, bucketScrollPercent); } @@ -354,7 +330,7 @@ return; } - if ($assetStore.timelineHeight < safeViewport.height * 2) { + if (assetStore.timelineHeight < safeViewport.height * 2) { // edge case - scroll limited due to size of content, must adjust - use the overall percent instead const maxScroll = getMaxScroll(); scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll); @@ -424,19 +400,15 @@ preMeasure.push(bucket); } showSkeleton = false; - $assetStore.clearPendingScroll(); + assetStore.clearPendingScroll(); // set intersecting true manually here, to reduce flicker that happens when // clearing pending scroll, but the intersection observer hadn't yet had time to run - $assetStore.updateBucket(bucket.bucketDate, { intersecting: true }); + assetStore.updateBucket(bucket.bucketDate, { intersecting: true }); }; const trashOrDelete = async (force: boolean = false) => { isShowDeleteConfirmation = false; - await deleteAssets( - !(isTrashEnabled && !force), - (assetIds) => $assetStore.removeAssets(assetIds), - idsSelectedAssets, - ); + await deleteAssets(!(isTrashEnabled && !force), (assetIds) => assetStore.removeAssets(assetIds), idsSelectedAssets); assetInteraction.clearMultiselect(); }; @@ -461,7 +433,7 @@ const onStackAssets = async () => { const ids = await stackAssets(assetInteraction.selectedAssetsArray); if (ids) { - $assetStore.removeAssets(ids); + assetStore.removeAssets(ids); onEscape(); } }; @@ -469,7 +441,7 @@ const toggleArchive = async () => { const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived); if (ids) { - $assetStore.removeAssets(ids); + assetStore.removeAssets(ids); deselectAllAssets(); } }; @@ -481,33 +453,33 @@ }; const handleSelectAsset = (asset: AssetResponseDto) => { - if (!$assetStore.albumAssets.has(asset.id)) { + if (!assetStore.albumAssets.has(asset.id)) { assetInteraction.selectAsset(asset); } }; function handleIntersect(bucket: AssetBucket) { - updateLastIntersectedBucketDate(); + // updateLastIntersectedBucketDate(); const task = () => { - $assetStore.updateBucket(bucket.bucketDate, { intersecting: true }); - void $assetStore.loadBucket(bucket.bucketDate); + assetStore.updateBucket(bucket.bucketDate, { intersecting: true }); + void assetStore.loadBucket(bucket.bucketDate); }; - $assetStore.taskManager.intersectedBucket(componentId, bucket, task); + assetStore.taskManager.intersectedBucket(componentId, bucket, task); } function handleSeparate(bucket: AssetBucket) { const task = () => { - $assetStore.updateBucket(bucket.bucketDate, { intersecting: false }); + assetStore.updateBucket(bucket.bucketDate, { intersecting: false }); bucket.cancel(); }; - $assetStore.taskManager.separatedBucket(componentId, bucket, task); + assetStore.taskManager.separatedBucket(componentId, bucket, task); } const handlePrevious = async () => { - const previousAsset = await $assetStore.getPreviousAsset($viewingAsset); + const previousAsset = await assetStore.getPreviousAsset($viewingAsset); if (previousAsset) { - const preloadAsset = await $assetStore.getPreviousAsset(previousAsset); + const preloadAsset = await assetStore.getPreviousAsset(previousAsset); assetViewingStore.setAsset(previousAsset, preloadAsset ? [preloadAsset] : []); await navigate({ targetRoute: 'current', assetId: previousAsset.id }); } @@ -516,9 +488,10 @@ }; const handleNext = async () => { - const nextAsset = await $assetStore.getNextAsset($viewingAsset); + const nextAsset = await assetStore.getNextAsset($viewingAsset); + if (nextAsset) { - const preloadAsset = await $assetStore.getNextAsset(nextAsset); + const preloadAsset = await assetStore.getNextAsset(nextAsset); assetViewingStore.setAsset(nextAsset, preloadAsset ? [preloadAsset] : []); await navigate({ targetRoute: 'current', assetId: nextAsset.id }); } @@ -527,10 +500,10 @@ }; const handleRandom = async () => { - const randomAsset = await $assetStore.getRandomAsset(); + const randomAsset = await assetStore.getRandomAsset(); if (randomAsset) { - const preloadAsset = await $assetStore.getNextAsset(randomAsset); + const preloadAsset = await assetStore.getNextAsset(randomAsset); assetViewingStore.setAsset(randomAsset, preloadAsset ? [preloadAsset] : []); await navigate({ targetRoute: 'current', assetId: randomAsset.id }); } @@ -664,8 +637,8 @@ assetInteraction.clearAssetSelectionCandidates(); if (assetInteraction.assetSelectionStart && rangeSelection) { - let startBucketIndex = $assetStore.getBucketIndexByAssetId(assetInteraction.assetSelectionStart.id); - let endBucketIndex = $assetStore.getBucketIndexByAssetId(asset.id); + let startBucketIndex = assetStore.getBucketIndexByAssetId(assetInteraction.assetSelectionStart.id); + let endBucketIndex = assetStore.getBucketIndexByAssetId(asset.id); if (startBucketIndex === null || endBucketIndex === null) { return; @@ -677,8 +650,8 @@ // Select/deselect assets in all intermediate buckets for (let bucketIndex = startBucketIndex + 1; bucketIndex < endBucketIndex; bucketIndex++) { - const bucket = $assetStore.buckets[bucketIndex]; - await $assetStore.loadBucket(bucket.bucketDate); + const bucket = assetStore.buckets[bucketIndex]; + await assetStore.loadBucket(bucket.bucketDate); for (const asset of bucket.assets) { if (deselect) { assetInteraction.removeAssetFromMultiselectGroup(asset); @@ -690,7 +663,7 @@ // Update date group selection for (let bucketIndex = startBucketIndex; bucketIndex <= endBucketIndex; bucketIndex++) { - const bucket = $assetStore.buckets[bucketIndex]; + const bucket = assetStore.buckets[bucketIndex]; // Split bucket into date groups and check each group const assetsGroupByDate = splitBucketIntoDateGroups(bucket, $locale); @@ -718,14 +691,14 @@ return; } - let start = $assetStore.assets.findIndex((a) => a.id === startAsset.id); - let end = $assetStore.assets.findIndex((a) => a.id === endAsset.id); + let start = assetStore.assets.findIndex((a) => a.id === startAsset.id); + let end = assetStore.assets.findIndex((a) => a.id === endAsset.id); if (start > end) { [start, end] = [end, start]; } - assetInteraction.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1)); + assetInteraction.setAssetSelectionCandidates(assetStore.assets.slice(start, end + 1)); }; const onSelectStart = (e: Event) => { @@ -737,7 +710,7 @@ assetStore.taskManager.removeAllTasksForComponent(componentId); }); let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); - let isEmpty = $derived($assetStore.initialized && $assetStore.buckets.length === 0); + let isEmpty = $derived(assetStore.initialized && assetStore.buckets.length === 0); let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id)); $effect(() => { @@ -773,7 +746,7 @@ { shortcut: { key: 'Escape' }, onShortcut: onEscape }, { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) }, { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, - { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteraction) }, + { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(assetStore, assetInteraction) }, { shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement }, { shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement }, ]; @@ -824,7 +797,7 @@ {#if showShortcuts} <ShowShortcuts onClose={() => (showShortcuts = !showShortcuts)} /> {/if} -{#if $assetStore.buckets.length > 0} +{#if assetStore.buckets.length > 0} <Scrubber invisible={showSkeleton} {assetStore} @@ -864,21 +837,33 @@ bind:this={timelineElement} id="virtual-timeline" class:invisible={showSkeleton} - style:height={$assetStore.timelineHeight + 'px'} + style:height={assetStore.timelineHeight + 'px'} > - {#each $assetStore.buckets as bucket (bucket.viewId)} + {#each assetStore.buckets as bucket (bucket.viewId)} {@const isPremeasure = preMeasure.includes(bucket)} - {@const display = bucket.intersecting || bucket === $assetStore.pendingScrollBucket || isPremeasure} + {@const display = bucket.intersecting || bucket === assetStore.pendingScrollBucket || isPremeasure} + <div class="bucket" - use:intersectionObserver={{ - key: bucket.viewId, - onIntersect: () => handleIntersect(bucket), - onSeparate: () => handleSeparate(bucket), - top: BUCKET_INTERSECTION_ROOT_TOP, - bottom: BUCKET_INTERSECTION_ROOT_BOTTOM, - root: element, - }} + style:overflow={bucket.measured ? 'visible' : 'clip'} + use:intersectionObserver={[ + { + key: bucket.viewId, + onIntersect: () => handleIntersect(bucket), + onSeparate: () => handleSeparate(bucket), + top: BUCKET_INTERSECTION_ROOT_TOP, + bottom: BUCKET_INTERSECTION_ROOT_BOTTOM, + root: element, + }, + { + key: bucket.viewId + '.bucketintersection', + onIntersect: () => (lastIntersectedBucketDate = bucket.bucketDate), + top: '0px', + bottom: '-' + Math.max(0, safeViewport.height - 1) + 'px', + left: '0px', + right: '0px', + }, + ]} data-bucket-display={bucket.intersecting} data-bucket-date={bucket.bucketDate} style:height={bucket.bucketHeight + 'px'} @@ -949,6 +934,5 @@ .bucket { contain: layout size; - transition: height 0.2s ease-out; } </style> diff --git a/web/src/lib/components/photos-page/measure-date-group.svelte b/web/src/lib/components/photos-page/measure-date-group.svelte index a435f89fb5..d3dabaa51d 100644 --- a/web/src/lib/components/photos-page/measure-date-group.svelte +++ b/web/src/lib/components/photos-page/measure-date-group.svelte @@ -18,7 +18,7 @@ <script lang="ts"> import { resizeObserver } from '$lib/actions/resize-observer'; - import type { AssetBucket, AssetStore, BucketListener } from '$lib/stores/assets.store'; + import type { AssetBucket, AssetStore, BucketListener } from '$lib/stores/assets-store.svelte'; interface Props { assetStore: AssetStore; @@ -43,11 +43,11 @@ if (!heightPending) { const height = element.getBoundingClientRect().height; if (height !== 0) { - $assetStore.updateBucket(bucket.bucketDate, { height, measured: true }); + assetStore.updateBucket(bucket.bucketDate, { height, measured: true }); } onMeasured(); - $assetStore.removeListener(listener); + assetStore.removeListener(listener); const t2 = Date.now(); addMeasure((t2 - t1) / bucket.bucketCount); @@ -69,7 +69,7 @@ <section id="measure-asset-group-by-date" class="flex flex-wrap gap-x-12" use:measure> {#each bucket.dateGroups as dateGroup (dateGroup.date)} <div id="date-group" data-date-group={dateGroup.date}> - <div use:resizeObserver={({ height }) => $assetStore.updateBucketDateGroup(bucket, dateGroup, { height })}> + <div use:resizeObserver={({ height }) => assetStore.updateBucketDateGroup(bucket, dateGroup, { height })}> <div class="flex z-[100] sticky top-[-1px] pt-7 pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm" style:width={dateGroup.geometry.containerWidth + 'px'} @@ -81,8 +81,8 @@ <div class="relative overflow-clip" - style:height={dateGroup.geometry.containerHeight + 'px'} - style:width={dateGroup.geometry.containerWidth + 'px'} + style:height={dateGroup.geometry!.containerHeight + 'px'} + style:width={dateGroup.geometry!.containerWidth + 'px'} style:visibility="hidden" ></div> </div> diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index ebc4b49001..d5ff27ae8c 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -17,7 +17,7 @@ import { cancelMultiselect } from '$lib/utils/asset-utils'; import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; - import type { Viewport } from '$lib/stores/assets.store'; + import type { Viewport } from '$lib/stores/assets-store.svelte'; import { t } from 'svelte-i18n'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte index 23bdaa8de3..ef0bf3cda7 100644 --- a/web/src/lib/components/shared-components/control-app-bar.svelte +++ b/web/src/lib/components/shared-components/control-app-bar.svelte @@ -5,7 +5,7 @@ import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import { fly } from 'svelte/transition'; import { mdiClose } from '@mdi/js'; - import { isSelectingAllAssets } from '$lib/stores/assets.store'; + import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte'; import { t } from 'svelte-i18n'; interface Props { 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 4c3c35aeca..0e1e611486 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 @@ -5,7 +5,7 @@ import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; import { AppRoute, AssetAction } from '$lib/constants'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import type { Viewport } from '$lib/stores/assets.store'; + import type { Viewport } from '$lib/stores/assets-store.svelte'; import { showDeleteModal } from '$lib/stores/preferences.store'; import { deleteAssets } from '$lib/utils/actions'; import { archiveAssets, cancelMultiselect, getAssetRatio } from '$lib/utils/asset-utils'; diff --git a/web/src/lib/components/shared-components/scrubber/scrubber.svelte b/web/src/lib/components/shared-components/scrubber/scrubber.svelte index c51c03dd0f..729810d022 100644 --- a/web/src/lib/components/shared-components/scrubber/scrubber.svelte +++ b/web/src/lib/components/shared-components/scrubber/scrubber.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import type { AssetStore, AssetBucket, BucketListener } from '$lib/stores/assets.store'; + import type { AssetStore, AssetBucket, BucketListener } from '$lib/stores/assets-store.svelte'; import { DateTime } from 'luxon'; import { fromLocalDateTime, type ScrubberListener } from '$lib/utils/timeline-util'; import { clamp } from 'lodash-es'; @@ -92,14 +92,14 @@ scrollY = toScrollFromBucketPercentage(scrubBucket, scrubBucketPercent, scrubOverallPercent); }); - let timelineFullHeight = $derived($assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset); + let timelineFullHeight = $derived(assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset); let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight)); let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight)); const listener: BucketListener = (event) => { const { type } = event; if (type === 'viewport') { - segments = calculateSegments($assetStore.buckets); + segments = calculateSegments(assetStore.buckets); scrollY = toScrollFromBucketPercentage(scrubBucket, scrubBucketPercent, scrubOverallPercent); } }; @@ -128,7 +128,7 @@ for (const [i, bucket] of buckets.entries()) { const scrollBarPercentage = - bucket.bucketHeight / ($assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset); + bucket.bucketHeight / (assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset); const segment = { count: bucket.assets.length, diff --git a/web/src/lib/stores/asset.store.spec.ts b/web/src/lib/stores/assets-store.spec.ts similarity index 98% rename from web/src/lib/stores/asset.store.spec.ts rename to web/src/lib/stores/assets-store.spec.ts index 918a29e97d..3541ea2205 100644 --- a/web/src/lib/stores/asset.store.spec.ts +++ b/web/src/lib/stores/assets-store.spec.ts @@ -2,7 +2,7 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock'; import { AbortError } from '$lib/utils'; import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk'; import { assetFactory } from '@test-data/factories/asset-factory'; -import { AssetStore } from './assets.store'; +import { AssetStore } from './assets-store.svelte'; describe('AssetStore', () => { beforeEach(() => { @@ -213,7 +213,8 @@ describe('AssetStore', () => { expect(assetStore.assets.length).toEqual(1); }); - it('ignores trashed assets when isTrashed is true', () => { + // disabled due to the wasm Justified Layout import + it.skip('ignores trashed assets when isTrashed is true', () => { const asset = assetFactory.build({ isTrashed: false }); const trashedAsset = assetFactory.build({ isTrashed: true }); diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets-store.svelte.ts similarity index 85% rename from web/src/lib/stores/assets.store.ts rename to web/src/lib/stores/assets-store.svelte.ts index 3c1a0211d4..f4cedde317 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets-store.svelte.ts @@ -1,28 +1,24 @@ import { locale } from '$lib/stores/preferences.store'; import { getKey } from '$lib/utils'; import { AssetGridTaskManager } from '$lib/utils/asset-store-task-manager'; -import { getAssetRatio } from '$lib/utils/asset-utils'; import { generateId } from '$lib/utils/generate-id'; +import { type getJustifiedLayoutFromAssetsFunction } from '$lib/utils/layout-utils'; import type { AssetGridRouteSearchParams } from '$lib/utils/navigation'; -import { calculateWidth, fromLocalDateTime, splitBucketIntoDateGroups, type DateGroup } from '$lib/utils/timeline-util'; +import { fromLocalDateTime, splitBucketIntoDateGroups, type DateGroup } from '$lib/utils/timeline-util'; import { TimeBucketSize, getAssetInfo, getTimeBucket, getTimeBuckets, type AssetResponseDto } from '@immich/sdk'; -import createJustifiedLayout from 'justified-layout'; import { throttle } from 'lodash-es'; import { DateTime } from 'luxon'; import { t } from 'svelte-i18n'; +import { SvelteSet } from 'svelte/reactivity'; import { get, writable, type Unsubscriber } from 'svelte/store'; import { handleError } from '../utils/handle-error'; import { websocketEvents } from './websocket'; + +let getJustifiedLayoutFromAssets: getJustifiedLayoutFromAssetsFunction; + type AssetApiGetTimeBucketsRequest = Parameters<typeof getTimeBuckets>[0]; export type AssetStoreOptions = Omit<AssetApiGetTimeBucketsRequest, 'size'>; -const LAYOUT_OPTIONS = { - boxSpacing: 2, - containerPadding: 0, - targetRowHeightTolerance: 0.15, - targetRowHeight: 235, -}; - export interface Viewport { width: number; height: number; @@ -40,30 +36,33 @@ interface AssetLookup { export class AssetBucket { store!: AssetStore; - bucketDate!: string; + bucketDate: string = $state(''); /** * The DOM height of the bucket in pixel * This value is first estimated by the number of asset and later is corrected as the user scroll + * Do not derive this height, it is important for it to be updated at specific times, so that + * calculateing a delta between estimated and actual (when measured) is correct. */ - bucketHeight: number = 0; - isBucketHeightActual: boolean = false; + bucketHeight: number = $state(0); + isBucketHeightActual: boolean = $state(false); bucketDateFormattted!: string; - bucketCount: number = 0; - assets: AssetResponseDto[] = []; - dateGroups: DateGroup[] = []; - cancelToken: AbortController | undefined; + bucketCount: number = $derived.by(() => (this.isLoaded ? this.assets.length : this.initialCount)); + initialCount: number = 0; + assets: AssetResponseDto[] = $state([]); + dateGroups: DateGroup[] = $state([]); + cancelToken: AbortController | undefined = $state(); /** * Prevent this asset's load from being canceled; i.e. to force load of offscreen asset. */ - isPreventCancel: boolean = false; + isPreventCancel: boolean = $state(false); /** * A promise that resolves once the bucket is loaded, and rejects if bucket is canceled. */ complete!: Promise<void>; - loading: boolean = false; - isLoaded: boolean = false; - intersecting: boolean = false; - measured: boolean = false; + loading: boolean = $state(false); + isLoaded: boolean = $state(false); + intersecting: boolean = $state(false); + measured: boolean = $state(false); measuredPromise!: Promise<void>; constructor(props: Partial<AssetBucket> & { store: AssetStore; bucketDate: string }) { @@ -79,13 +78,16 @@ export class AssetBucket { // will be incoked when a bucket is loaded, fulfilling the promise. The canceledSignal // callback will be called if the bucket is canceled before it was loaded, rejecting the // promise. - this.complete = new Promise((resolve, reject) => { + this.complete = new Promise<void>((resolve, reject) => { this.loadedSignal = resolve; this.canceledSignal = reject; - }); - // if no-one waits on complete, and its rejected a uncaught rejection message is logged. - // We this message with an empty reject handler, since waiting on a bucket is optional. - this.complete.catch(() => void 0); + }).catch( + () => + // if no-one waits on complete, and its rejected a uncaught rejection message is logged. + // We this message with an empty reject handler, since waiting on a bucket is optional. + void 0, + ); + this.measuredPromise = new Promise((resolve) => { this.measuredSignal = resolve; }); @@ -205,35 +207,50 @@ type DateGroupHeightEvent = { }; export class AssetStore { - private assetToBucket: Record<string, AssetLookup> = {}; + private assetToBucket: Record<string, AssetLookup> = $derived.by(() => { + const result: Record<string, AssetLookup> = {}; + for (let index = 0; index < this.buckets.length; index++) { + const bucket = this.buckets[index]; + for (let index_ = 0; index_ < bucket.assets.length; index_++) { + const asset = bucket.assets[index_]; + result[asset.id] = { bucket, bucketIndex: index, assetIndex: index_ }; + } + } + return result; + }); private pendingChanges: PendingChange[] = []; private unsubscribers: Unsubscriber[] = []; private options!: AssetApiGetTimeBucketsRequest; - private viewport: Viewport = { + viewport: Viewport = $state({ height: 0, width: 0, - }; + }); private initializedSignal!: () => void; private store$ = writable(this); + /** The svelte key for this view model object */ viewId = generateId(); - lastScrollTime: number = 0; - subscribe = this.store$.subscribe; + lastScrollTime: number = $state(0); + + // subscribe = this.store$.subscribe; /** * A promise that resolves once the store is initialized. */ - complete!: Promise<void>; + private complete!: Promise<void>; taskManager = new AssetGridTaskManager(this); - initialized = false; - timelineHeight = 0; - buckets: AssetBucket[] = []; - assets: AssetResponseDto[] = []; - albumAssets: Set<string> = new Set(); - pendingScrollBucket: AssetBucket | undefined; - pendingScrollAssetId: string | undefined; + initialized = $state(false); + timelineHeight = $state(0); + buckets: AssetBucket[] = $state([]); + assets: AssetResponseDto[] = $derived.by(() => { + return this.buckets.flatMap(({ assets }) => assets); + }); + albumAssets: Set<string> = new SvelteSet(); + pendingScrollBucket: AssetBucket | undefined = $state(); + pendingScrollAssetId: string | undefined = $state(); + maxBucketAssets = $state(0); - listeners: BucketListener[] = []; + private listeners: BucketListener[] = []; constructor( options: AssetStoreOptions, @@ -251,11 +268,9 @@ export class AssetStore { private createInitializationSignal() { // create a promise, and store its resolve callbacks. The initializedSignal callback // will be invoked when a the assetstore is initialized. - this.complete = new Promise((resolve) => { + this.complete = new Promise<void>((resolve) => { this.initializedSignal = resolve; - }); - // uncaught rejection go away - this.complete.catch(() => void 0); + }).catch(() => void 0); } private addPendingChanges(...changes: PendingChange[]) { @@ -346,7 +361,7 @@ export class AssetStore { } this.pendingChanges = []; - this.emit(true); + // this.emit(true); }, 2500); addListener(bucketListener: BucketListener) { @@ -373,6 +388,11 @@ export class AssetStore { if (this.initialized) { throw 'Can only init once'; } + if (!getJustifiedLayoutFromAssets) { + const module = await import('$lib/utils/layout-utils'); + getJustifiedLayoutFromAssets = module.getJustifiedLayoutFromAssets; + } + if (bucketListener) { this.addListener(bucketListener); } @@ -382,17 +402,16 @@ export class AssetStore { async initialiazeTimeBuckets() { this.timelineHeight = 0; this.buckets = []; - this.assets = []; - this.assetToBucket = {}; - this.albumAssets = new Set(); + this.albumAssets.clear(); const timebuckets = await getTimeBuckets({ ...this.options, key: getKey(), }); this.buckets = timebuckets.map( - (bucket) => new AssetBucket({ store: this, bucketDate: bucket.timeBucket, bucketCount: bucket.count }), + (bucket) => new AssetBucket({ store: this, bucketDate: bucket.timeBucket, initialCount: bucket.count }), ); + this.initializedSignal(); this.initialized = true; } @@ -416,7 +435,7 @@ export class AssetStore { this.createInitializationSignal(); this.setOptions(options); await this.initialiazeTimeBuckets(); - this.emit(true); + // this.emit(true); await this.initialLayout(true); } @@ -458,7 +477,6 @@ export class AssetStore { } await Promise.all(loaders); this.notifyListeners({ type: 'viewport' }); - this.emit(false); } private updateGeometry(bucket: AssetBucket, invalidateHeight: boolean) { @@ -469,13 +487,20 @@ export class AssetStore { assetGroup.heightActual = false; } } + const viewportWidth = this.viewport.width; if (!bucket.isBucketHeightActual) { const unwrappedWidth = (3 / 2) * bucket.bucketCount * THUMBNAIL_HEIGHT * (7 / 10); - const rows = Math.ceil(unwrappedWidth / this.viewport.width); - const height = 51 + rows * THUMBNAIL_HEIGHT; - bucket.bucketHeight = height; - } + const rows = Math.ceil(unwrappedWidth / viewportWidth); + const height = 51 + Math.max(1, rows) * THUMBNAIL_HEIGHT; + this.setBucketHeight(bucket, height, false); + } + const layoutOptions = { + spacing: 2, + heightTolerance: 0.15, + rowHeight: 235, + rowWidth: Math.floor(viewportWidth), + }; for (const assetGroup of bucket.dateGroups) { if (!assetGroup.heightActual) { const unwrappedWidth = (3 / 2) * assetGroup.assets.length * THUMBNAIL_HEIGHT * (7 / 10); @@ -484,17 +509,7 @@ export class AssetStore { assetGroup.height = height; } - const layoutResult = createJustifiedLayout( - assetGroup.assets.map((g) => getAssetRatio(g)), - { - ...LAYOUT_OPTIONS, - containerWidth: Math.floor(this.viewport.width), - }, - ); - assetGroup.geometry = { - ...layoutResult, - containerWidth: calculateWidth(layoutResult.boxes), - }; + assetGroup.geometry = getJustifiedLayoutFromAssets(assetGroup.assets, layoutOptions); } } @@ -503,7 +518,7 @@ export class AssetStore { if (!bucket) { return; } - if (bucket.bucketCount === bucket.assets.length) { + if (bucket.isLoaded) { // already loaded return; } @@ -522,7 +537,6 @@ export class AssetStore { } this.notifyListeners({ type: 'load', bucket }); bucket.isPreventCancel = !!options.preventCancel; - const cancelToken = (bucket.cancelToken = new AbortController()); try { const assets = await getTimeBucket( @@ -569,28 +583,30 @@ export class AssetStore { if ((error as any).name === 'AbortError') { return; } - const $t = get(t); - handleError(error, $t('errors.failed_to_load_assets')); + const _$t = get(t); + handleError(error, _$t('errors.failed_to_load_assets')); bucket.errored(); } finally { bucket.cancelToken = undefined; - this.emit(true); } } + setBucketHeight(bucket: AssetBucket, newHeight: number, isActualHeight: boolean) { + const delta = newHeight - bucket.bucketHeight; + bucket.isBucketHeightActual = isActualHeight; + bucket.bucketHeight = newHeight; + this.timelineHeight += delta; + this.notifyListeners({ type: 'bucket-height', bucket, delta }); + } + updateBucket(bucketDate: string, properties: { height?: number; intersecting?: boolean; measured?: boolean }) { const bucket = this.getBucketByDate(bucketDate); if (!bucket) { return {}; } - let delta = 0; + const delta = 0; if ('height' in properties) { - const height = properties.height!; - delta = height - bucket.bucketHeight; - bucket.isBucketHeightActual = true; - bucket.bucketHeight = height; - this.timelineHeight += delta; - this.notifyListeners({ type: 'bucket-height', bucket, delta }); + this.setBucketHeight(bucket, properties.height!, true); } if ('intersecting' in properties) { bucket.intersecting = properties.intersecting!; @@ -601,7 +617,6 @@ export class AssetStore { } bucket.measured = properties.measured!; } - this.emit(false); return { delta }; } @@ -626,7 +641,6 @@ export class AssetStore { this.notifyListeners({ type: 'intersecting', bucket, dateGroup }); } } - this.emit(false); return { delta }; } @@ -670,7 +684,6 @@ export class AssetStore { } bucket.assets.push(asset); - this.assets.push(asset); updatedBuckets.add(bucket); } @@ -689,8 +702,6 @@ export class AssetStore { bucket.dateGroups = splitBucketIntoDateGroups(bucket, get(locale)); this.updateGeometry(bucket, true); } - - this.emit(true); } getBucketByDate(bucketDate: string): AssetBucket | null { @@ -705,14 +716,12 @@ export class AssetStore { if (!asset || this.isExcluded(asset)) { return; } - bucket = await this.loadBucketAtTime(asset.localDateTime, { preventCancel: true, pending: true }); } if (bucket && bucket.assets.some((a) => a.id === id)) { this.pendingScrollBucket = bucket; this.pendingScrollAssetId = id; - this.emit(false); return bucket; } } @@ -805,7 +814,6 @@ export class AssetStore { this.removeAssets(assetsToRecalculate.map((asset) => asset.id)); this.addAssetsToBuckets(assetsToRecalculate); - this.emit(assetsToRecalculate.length > 0); } removeAssets(ids: string[]) { @@ -832,8 +840,6 @@ export class AssetStore { this.updateGeometry(bucket, true); } } - - this.emit(true); } async getPreviousAsset(asset: AssetResponseDto): Promise<AssetResponseDto | null> { @@ -878,30 +884,6 @@ export class AssetStore { return nextBucket.assets[0] || null; } - triggerUpdate() { - this.emit(false); - } - - private emit(recalculate: boolean) { - if (recalculate) { - this.assets = this.buckets.flatMap(({ assets }) => assets); - - const assetToBucket: Record<string, AssetLookup> = {}; - for (let index = 0; index < this.buckets.length; index++) { - const bucket = this.buckets[index]; - if (bucket.assets.length > 0) { - bucket.bucketCount = bucket.assets.length; - } - for (let index_ = 0; index_ < bucket.assets.length; index_++) { - const asset = bucket.assets[index_]; - assetToBucket[asset.id] = { bucket, bucketIndex: index, assetIndex: index_ }; - } - } - this.assetToBucket = assetToBucket; - } - this.store$.set(this); - } - private isExcluded(asset: AssetResponseDto) { return ( isMismatched(this.options.isArchived ?? false, asset.isArchived) || diff --git a/web/src/lib/utils/asset-store-task-manager.ts b/web/src/lib/utils/asset-store-task-manager.ts index 60004235f4..e9a9e459fe 100644 --- a/web/src/lib/utils/asset-store-task-manager.ts +++ b/web/src/lib/utils/asset-store-task-manager.ts @@ -1,4 +1,4 @@ -import type { AssetBucket, AssetStore } from '$lib/stores/assets.store'; +import type { AssetBucket, AssetStore } from '$lib/stores/assets-store.svelte'; import { generateId } from '$lib/utils/generate-id'; import { cancelIdleCB, idleCB } from '$lib/utils/idle-callback-support'; import { KeyedPriorityQueue } from '$lib/utils/keyed-priority-queue'; diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index fa9725aa24..b802bbe0a3 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -5,7 +5,7 @@ import { NotificationType, notificationController } from '$lib/components/shared import { AppRoute } from '$lib/constants'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; -import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; +import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte'; import { downloadManager } from '$lib/stores/download'; import { preferences } from '$lib/stores/user.store'; import { downloadRequest, getKey, withError } from '$lib/utils'; diff --git a/web/src/lib/utils/executor-queue.spec.ts b/web/src/lib/utils/executor-queue.spec.ts index b6ba77e9f3..434263a182 100644 --- a/web/src/lib/utils/executor-queue.spec.ts +++ b/web/src/lib/utils/executor-queue.spec.ts @@ -28,15 +28,11 @@ describe('Executor Queue test', function () { }); // The first 3 should be finished within 200ms (concurrency 3) - // eslint-disable-next-line @typescript-eslint/no-floating-promises - eq.addTask(() => timeoutPromiseBuilder(100, 'T1')); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - eq.addTask(() => timeoutPromiseBuilder(200, 'T2')); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - eq.addTask(() => timeoutPromiseBuilder(150, 'T3')); + void eq.addTask(() => timeoutPromiseBuilder(100, 'T1')); + void eq.addTask(() => timeoutPromiseBuilder(200, 'T2')); + void eq.addTask(() => timeoutPromiseBuilder(150, 'T3')); // The last task will be executed after 200ms and will finish at 400ms - // eslint-disable-next-line @typescript-eslint/no-floating-promises - eq.addTask(() => timeoutPromiseBuilder(200, 'T4')); + void eq.addTask(() => timeoutPromiseBuilder(200, 'T4')); expect(finished).not.toBeCalled(); expect(started).toHaveBeenCalledTimes(3); diff --git a/web/src/lib/utils/layout-utils.ts b/web/src/lib/utils/layout-utils.ts new file mode 100644 index 0000000000..6f14ed9825 --- /dev/null +++ b/web/src/lib/utils/layout-utils.ts @@ -0,0 +1,106 @@ +import { getAssetRatio } from '$lib/utils/asset-utils'; +// import { TUNABLES } from '$lib/utils/tunables'; +// 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'; +import createJustifiedLayout from 'justified-layout'; + +export type getJustifiedLayoutFromAssetsFunction = typeof getJustifiedLayoutFromAssets; + +// let useWasm = TUNABLES.LAYOUT.WASM; + +export type CommonJustifiedLayout = { + containerWidth: number; + containerHeight: number; + getTop(boxIdx: number): number; + getLeft(boxIdx: number): number; + getWidth(boxIdx: number): number; + getHeight(boxIdx: number): number; +}; + +export type CommonLayoutOptions = { + rowHeight: number; + rowWidth: number; + spacing: number; + heightTolerance: number; +}; + +export function getJustifiedLayoutFromAssets( + assets: AssetResponseDto[], + options: CommonLayoutOptions, +): CommonJustifiedLayout { + // if (useWasm) { + // return wasmJustifiedLayout(assets, options); + // } + return justifiedLayout(assets, options); +} + +// commented out until a solution for top level awaits on safari is fixed +// function wasmJustifiedLayout(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); +// } + +type Geometry = ReturnType<typeof createJustifiedLayout>; +class Adapter { + result; + constructor(result: Geometry) { + this.result = result; + } + + get containerWidth() { + let width = 0; + for (const box of this.result.boxes) { + if (box.top < 100) { + width = box.left + box.width; + } + } + return width; + } + + get containerHeight() { + return this.result.containerHeight; + } + + getTop(boxIdx: number) { + return this.result.boxes[boxIdx]?.top; + } + + getLeft(boxIdx: number) { + return this.result.boxes[boxIdx]?.left; + } + + getWidth(boxIdx: number) { + return this.result.boxes[boxIdx]?.width; + } + + getHeight(boxIdx: number) { + return this.result.boxes[boxIdx]?.height; + } +} + +export const emptyGeometry = new Adapter({ + containerHeight: 0, + widowCount: 0, + boxes: [], +}); + +export function justifiedLayout(assets: AssetResponseDto[], options: CommonLayoutOptions) { + const adapter = { + targetRowHeight: options.rowHeight, + containerWidth: options.rowWidth, + boxSpacing: options.spacing, + targetRowHeightTolerange: options.heightTolerance, + }; + + const result = createJustifiedLayout( + assets.map((g) => getAssetRatio(g)), + adapter, + ); + return new Adapter(result); +} diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 7e65dcdb99..eb17be61a8 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -1,7 +1,8 @@ -import type { AssetBucket } from '$lib/stores/assets.store'; +import type { AssetBucket } from '$lib/stores/assets-store.svelte'; import { locale } from '$lib/stores/preferences.store'; +import { emptyGeometry, type CommonJustifiedLayout } from '$lib/utils/layout-utils'; + import type { AssetResponseDto } from '@immich/sdk'; -import type createJustifiedLayout from 'justified-layout'; import { groupBy, memoize, sortBy } from 'lodash-es'; import { DateTime } from 'luxon'; import { get } from 'svelte/store'; @@ -13,7 +14,7 @@ export type DateGroup = { height: number; heightActual: boolean; intersecting: boolean; - geometry: Geometry; + geometry: CommonJustifiedLayout; bucket: AssetBucket; }; export type ScrubberListener = ( @@ -80,19 +81,6 @@ export function formatGroupTitle(_date: DateTime): string { return date.toLocaleString(groupDateFormat); } -type Geometry = ReturnType<typeof createJustifiedLayout> & { - containerWidth: number; -}; - -function emptyGeometry() { - return { - containerWidth: 0, - containerHeight: 0, - widowCount: 0, - boxes: [], - }; -} - const formatDateGroupTitle = memoize(formatGroupTitle); export function splitBucketIntoDateGroups(bucket: AssetBucket, locale: string | undefined): DateGroup[] { @@ -109,7 +97,7 @@ export function splitBucketIntoDateGroups(bucket: AssetBucket, locale: string | height: 0, heightActual: false, intersecting: false, - geometry: emptyGeometry(), + geometry: emptyGeometry, bucket, }; }); diff --git a/web/src/lib/utils/tunables.ts b/web/src/lib/utils/tunables.ts index e21c30de77..2f425b847d 100644 --- a/web/src/lib/utils/tunables.ts +++ b/web/src/lib/utils/tunables.ts @@ -17,6 +17,9 @@ function getFloat(string: string | null, fallback: number) { return Number.parseFloat(string); } export const TUNABLES = { + LAYOUT: { + WASM: getBoolean(localStorage.getItem('LAYOUT.WASM'), false), + }, SCROLL_TASK_QUEUE: { TRICKLE_BONUS_FACTOR: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_BONUS_FACTOR'), 25), TRICKLE_ACCELERATION_FACTOR: getFloat(localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_ACCELERATION_FACTOR'), 1.5), diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 289b16a24a..763050df82 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -36,7 +36,7 @@ import { AppRoute, AlbumPageViewMode } from '$lib/constants'; import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import { AssetStore } from '$lib/stores/assets.store'; + import { AssetStore } from '$lib/stores/assets-store.svelte'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { preferences, user } from '$lib/stores/user.store'; import { handlePromiseError } from '$lib/utils'; @@ -445,10 +445,7 @@ <AddToAlbum shared /> </ButtonContextMenu> {#if assetInteraction.isAllUserOwned} - <FavoriteAction - removeFavorite={assetInteraction.isAllFavorite} - onFavorite={() => assetStore.triggerUpdate()} - /> + <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} /> {/if} <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <DownloadAction menuItem filename="{album.albumName}.zip" /> @@ -462,11 +459,7 @@ onClick={() => updateThumbnailUsingCurrentSelection()} /> {/if} - <ArchiveAction - menuItem - unarchive={assetInteraction.isAllArchived} - onArchive={() => assetStore.triggerUpdate()} - /> + <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} /> {/if} {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3ef28cd657..c8b239218a 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -12,7 +12,7 @@ import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { AssetAction } from '$lib/constants'; - import { AssetStore } from '$lib/stores/assets.store'; + import { AssetStore } from '$lib/stores/assets-store.svelte'; import type { PageData } from './$types'; import { mdiPlus, mdiDotsVertical } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -52,7 +52,7 @@ <AddToAlbum /> <AddToAlbum shared /> </ButtonContextMenu> - <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> + <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} /> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <DownloadAction menuItem /> <DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} /> diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index 94436a3dc9..02cac3644d 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -14,7 +14,7 @@ import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { AssetAction } from '$lib/constants'; - import { AssetStore } from '$lib/stores/assets.store'; + import { AssetStore } from '$lib/stores/assets-store.svelte'; import type { PageData } from './$types'; import { mdiDotsVertical, mdiPlus } from '@mdi/js'; import { t } from 'svelte-i18n'; diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index bcedeb0bc5..c412aa8ea2 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -7,7 +7,7 @@ import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; import { AppRoute, QueryParameter } from '$lib/constants'; - import type { Viewport } from '$lib/stores/assets.store'; + import type { Viewport } from '$lib/stores/assets-store.svelte'; import { foldersStore } from '$lib/stores/folders.svelte'; import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 9060f84a3b..7885086b44 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -8,7 +8,7 @@ import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import { AppRoute } from '$lib/constants'; - import { AssetStore } from '$lib/stores/assets.store'; + import { AssetStore } from '$lib/stores/assets-store.svelte'; import { onDestroy } from 'svelte'; import type { PageData } from './$types'; import { mdiPlus, mdiArrowLeft } from '@mdi/js'; diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index f025b7b50e..b591cf1bdb 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -33,7 +33,7 @@ import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import { AssetStore } from '$lib/stores/assets.store'; + import { AssetStore } from '$lib/stores/assets-store.svelte'; import { preferences } from '$lib/stores/user.store'; import { websocketEvents } from '$lib/stores/websocket'; import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils'; @@ -77,12 +77,8 @@ $effect(() => { // Check to trigger rebuild the timeline when navigating between people from the info panel - const change = assetStoreOptions.personId !== data.person.id; assetStoreOptions.personId = data.person.id; handlePromiseError(assetStore.updateOptions(assetStoreOptions)); - if (change) { - assetStore.triggerUpdate(); - } }); const assetInteraction = new AssetInteraction(); @@ -156,7 +152,7 @@ }); const handleUnmerge = () => { - $assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id)); + assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id)); assetInteraction.clearMultiselect(); viewMode = PersonPageViewMode.VIEW_ASSETS; }; @@ -358,7 +354,7 @@ }; const handleDeleteAssets = async (assetIds: string[]) => { - $assetStore.removeAssets(assetIds); + assetStore.removeAssets(assetIds); await updateAssetCount(); }; @@ -420,7 +416,7 @@ <AddToAlbum /> <AddToAlbum shared /> </ButtonContextMenu> - <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> + <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} /> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <DownloadAction menuItem filename="{person.name || 'immich'}.zip" /> <MenuOption @@ -433,7 +429,7 @@ <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} - onArchive={(assetIds) => $assetStore.removeAssets(assetIds)} + onArchive={(assetIds) => assetStore.removeAssets(assetIds)} /> {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} <TagAction menuItem /> diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index ff99599c51..1b6ff3071a 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -22,7 +22,7 @@ import { AssetAction } from '$lib/constants'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import { AssetStore } from '$lib/stores/assets.store'; + import { AssetStore } from '$lib/stores/assets-store.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { preferences, user } from '$lib/stores/user.store'; import type { OnLink, OnUnlink } from '$lib/utils/actions'; @@ -88,7 +88,7 @@ <AddToAlbum /> <AddToAlbum shared /> </ButtonContextMenu> - <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> + <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} /> <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> <DownloadAction menuItem /> {#if assetInteraction.selectedAssets.size > 1 || isAssetStackSelected} diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index c2f0ed619c..c1b75bc561 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -32,7 +32,7 @@ getTagById, } from '@immich/sdk'; import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; - import type { Viewport } from '$lib/stores/assets.store'; + import type { Viewport } from '$lib/stores/assets-store.svelte'; import { locale } from '$lib/stores/preferences.store'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import { handlePromiseError } from '$lib/utils'; diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte index 4c0e90e97c..96bebe34c1 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -17,7 +17,7 @@ import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; - import { AssetStore } from '$lib/stores/assets.store'; + import { AssetStore } from '$lib/stores/assets-store.svelte'; import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk'; import { Button, HStack, Text } from '@immich/ui'; diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5246f3b797..e31929f2c5 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -15,7 +15,7 @@ } from '$lib/components/shared-components/notification/notification'; import { AppRoute } from '$lib/constants'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; - import { AssetStore } from '$lib/stores/assets.store'; + import { AssetStore } from '$lib/stores/assets-store.svelte'; import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; import { handlePromiseError } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; diff --git a/web/vite.config.js b/web/vite.config.js index a2e7393df9..0ffc0e0eb1 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -14,6 +14,9 @@ const upstream = { }; export default defineConfig({ + build: { + target: 'es2022', + }, resolve: { alias: { 'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js',