mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	refactor(web): asset grid stores (#3464)
* Refactor asset grid stores * Iterate over buckets with for..of loop * Rebase on top of main branch changes
This commit is contained in:
		| @@ -43,13 +43,15 @@ | ||||
|   import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; | ||||
|   import { handleError } from '../../utils/handle-error'; | ||||
|   import { downloadArchive } from '../../utils/asset-utils'; | ||||
|   import { isViewingAssetStoreState } from '$lib/stores/asset-interaction.store'; | ||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||
|  | ||||
|   export let album: AlbumResponseDto; | ||||
|   export let sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||
|  | ||||
|   const { isAlbumAssetSelectionOpen } = albumAssetSelectionStore; | ||||
|  | ||||
|   let { isViewing: showAssetViewer } = assetViewingStore; | ||||
|  | ||||
|   let isShowAssetSelection = false; | ||||
|  | ||||
|   let isShowShareLinkModal = false; | ||||
| @@ -141,7 +143,7 @@ | ||||
|   }); | ||||
|  | ||||
|   const handleKeyboardPress = (event: KeyboardEvent) => { | ||||
|     if (!$isViewingAssetStoreState) { | ||||
|     if (!$showAssetViewer) { | ||||
|       switch (event.key) { | ||||
|         case 'Escape': | ||||
|           if (isMultiSelectionMode) { | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| <script lang="ts"> | ||||
|   import { assetInteractionStore, assetsInAlbumStoreState, selectedAssets } from '$lib/stores/asset-interaction.store'; | ||||
|   import { locale } from '$lib/stores/preferences.store'; | ||||
|   import { openFileUploadDialog } from '$lib/utils/file-uploader'; | ||||
|   import type { AssetResponseDto } from '@api'; | ||||
| @@ -9,14 +8,20 @@ | ||||
|   import Button from '../elements/buttons/button.svelte'; | ||||
|   import AssetGrid from '../photos-page/asset-grid.svelte'; | ||||
|   import ControlAppBar from '../shared-components/control-app-bar.svelte'; | ||||
|   import { createAssetStore } from '$lib/stores/assets.store'; | ||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; | ||||
|  | ||||
|   const dispatch = createEventDispatcher(); | ||||
|  | ||||
|   const assetStore = createAssetStore(); | ||||
|   const assetInteractionStore = createAssetInteractionStore(); | ||||
|   const { selectedAssets, assetsInAlbumState } = assetInteractionStore; | ||||
|  | ||||
|   export let albumId: string; | ||||
|   export let assetsInAlbum: AssetResponseDto[]; | ||||
|  | ||||
|   onMount(() => { | ||||
|     $assetsInAlbumStoreState = assetsInAlbum; | ||||
|     $assetsInAlbumState = assetsInAlbum; | ||||
|   }); | ||||
|  | ||||
|   const addSelectedAssets = async () => { | ||||
| @@ -64,6 +69,6 @@ | ||||
|     </svelte:fragment> | ||||
|   </ControlAppBar> | ||||
|   <section class="grid h-screen bg-immich-bg pl-[70px] pt-[100px] dark:bg-immich-dark-bg"> | ||||
|     <AssetGrid isAlbumSelectionMode={true} /> | ||||
|     <AssetGrid {assetStore} {assetInteractionStore} isAlbumSelectionMode={true} /> | ||||
|   </section> | ||||
| </section> | ||||
|   | ||||
| @@ -17,13 +17,14 @@ | ||||
|   import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; | ||||
|   import ProfileImageCropper from '../shared-components/profile-image-cropper.svelte'; | ||||
|  | ||||
|   import { assetStore } from '$lib/stores/assets.store'; | ||||
|   import { isShowDetail } from '$lib/stores/preferences.store'; | ||||
|   import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils'; | ||||
|   import NavigationArea from './navigation-area.svelte'; | ||||
|   import { browser } from '$app/environment'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import type { AssetStore } from '$lib/stores/assets.store'; | ||||
|  | ||||
|   export let assetStore: AssetStore | null = null; | ||||
|   export let asset: AssetResponseDto; | ||||
|   export let publicSharedKey = ''; | ||||
|   export let showNavigation = true; | ||||
| @@ -134,7 +135,7 @@ | ||||
|  | ||||
|       for (const asset of deletedAssets) { | ||||
|         if (asset.status == 'SUCCESS') { | ||||
|           assetStore.removeAsset(asset.id); | ||||
|           assetStore?.removeAsset(asset.id); | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
| @@ -158,7 +159,7 @@ | ||||
|       }); | ||||
|  | ||||
|       asset.isFavorite = data.isFavorite; | ||||
|       assetStore.updateAsset(asset.id, data.isFavorite); | ||||
|       assetStore?.updateAsset(asset.id, data.isFavorite); | ||||
|  | ||||
|       notificationController.show({ | ||||
|         type: NotificationType.Info, | ||||
|   | ||||
| @@ -1,28 +1,30 @@ | ||||
| <script lang="ts"> | ||||
|   import { get } from 'svelte/store'; | ||||
|   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||
|   import SelectAll from 'svelte-material-icons/SelectAll.svelte'; | ||||
|   import TimerSand from 'svelte-material-icons/TimerSand.svelte'; | ||||
|   import { assetInteractionStore } from '$lib/stores/asset-interaction.store'; | ||||
|   import { assetGridState, assetStore } from '$lib/stores/assets.store'; | ||||
|   import { handleError } from '../../../utils/handle-error'; | ||||
|   import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state'; | ||||
|   import { BucketPosition } from '$lib/models/asset-grid-state'; | ||||
|   import type { AssetStore } from '$lib/stores/assets.store'; | ||||
|   import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; | ||||
|  | ||||
|   export let assetStore: AssetStore; | ||||
|   export let assetInteractionStore: AssetInteractionStore; | ||||
|  | ||||
|   let selecting = false; | ||||
|  | ||||
|   const handleSelectAll = async () => { | ||||
|     try { | ||||
|       selecting = true; | ||||
|       let _assetGridState = new AssetGridState(); | ||||
|       assetGridState.subscribe((state) => { | ||||
|         _assetGridState = state; | ||||
|       }); | ||||
|  | ||||
|       for (let i = 0; i < _assetGridState.buckets.length; i++) { | ||||
|         await assetStore.getAssetsByBucket(_assetGridState.buckets[i].bucketDate, BucketPosition.Unknown); | ||||
|         for (const asset of _assetGridState.buckets[i].assets) { | ||||
|       const assetGridState = get(assetStore); | ||||
|       for (const bucket of assetGridState.buckets) { | ||||
|         await assetStore.getAssetsByBucket(bucket.bucketDate, BucketPosition.Unknown); | ||||
|         for (const asset of bucket.assets) { | ||||
|           assetInteractionStore.addAssetToMultiselectGroup(asset); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       selecting = false; | ||||
|     } catch (e) { | ||||
|       handleError(e, 'Error selecting all assets'); | ||||
|   | ||||
| @@ -1,13 +1,4 @@ | ||||
| <script lang="ts"> | ||||
|   import { | ||||
|     assetInteractionStore, | ||||
|     assetSelectionCandidates, | ||||
|     assetsInAlbumStoreState, | ||||
|     isMultiSelectStoreState, | ||||
|     selectedAssets, | ||||
|     selectedGroup, | ||||
|   } from '$lib/stores/asset-interaction.store'; | ||||
|   import { assetStore } from '$lib/stores/assets.store'; | ||||
|   import { locale } from '$lib/stores/preferences.store'; | ||||
|   import { getAssetRatio } from '$lib/utils/asset-utils'; | ||||
|   import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; | ||||
| @@ -19,6 +10,9 @@ | ||||
|   import CircleOutline from 'svelte-material-icons/CircleOutline.svelte'; | ||||
|   import { fly } from 'svelte/transition'; | ||||
|   import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; | ||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||
|   import type { AssetStore } from '$lib/stores/assets.store'; | ||||
|   import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; | ||||
|  | ||||
|   export let assets: AssetResponseDto[]; | ||||
|   export let bucketDate: string; | ||||
| @@ -26,6 +20,12 @@ | ||||
|   export let isAlbumSelectionMode = false; | ||||
|   export let viewportWidth: number; | ||||
|  | ||||
|   export let assetStore: AssetStore; | ||||
|   export let assetInteractionStore: AssetInteractionStore; | ||||
|  | ||||
|   const { selectedGroup, selectedAssets, assetsInAlbumState, assetSelectionCandidates, isMultiSelectState } = | ||||
|     assetInteractionStore; | ||||
|  | ||||
|   const dispatch = createEventDispatcher(); | ||||
|  | ||||
|   let isMouseOverGroup = false; | ||||
| @@ -94,10 +94,10 @@ | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if ($isMultiSelectStoreState) { | ||||
|     if ($isMultiSelectState) { | ||||
|       assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle); | ||||
|     } else { | ||||
|       assetInteractionStore.setViewingAsset(asset); | ||||
|       assetViewingStore.setAssetId(asset.id); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
| @@ -137,7 +137,7 @@ | ||||
|     // Show multi select icon on hover on date group | ||||
|     hoveredDateGroup = dateGroupTitle; | ||||
|  | ||||
|     if ($isMultiSelectStoreState) { | ||||
|     if ($isMultiSelectState) { | ||||
|       dispatch('selectAssetCandidates', { asset }); | ||||
|     } | ||||
|   }; | ||||
| @@ -207,9 +207,9 @@ | ||||
|               on:click={() => assetClickHandler(asset, assetsInDateGroup, dateGroupTitle)} | ||||
|               on:select={() => assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle)} | ||||
|               on:mouse-event={() => assetMouseEventHandler(dateGroupTitle, asset)} | ||||
|               selected={$selectedAssets.has(asset) || $assetsInAlbumStoreState.some(({ id }) => id === asset.id)} | ||||
|               selected={$selectedAssets.has(asset) || $assetsInAlbumState.some(({ id }) => id === asset.id)} | ||||
|               selectionCandidate={$assetSelectionCandidates.has(asset)} | ||||
|               disabled={$assetsInAlbumStoreState.some(({ id }) => id === asset.id)} | ||||
|               disabled={$assetsInAlbumState.some(({ id }) => id === asset.id)} | ||||
|               thumbnailWidth={box.width} | ||||
|               thumbnailHeight={box.height} | ||||
|             /> | ||||
|   | ||||
| @@ -1,15 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import { BucketPosition } from '$lib/models/asset-grid-state'; | ||||
|   import { | ||||
|     assetInteractionStore, | ||||
|     assetSelectionCandidates, | ||||
|     assetSelectionStart, | ||||
|     isMultiSelectStoreState, | ||||
|     isViewingAssetStoreState, | ||||
|     selectedAssets, | ||||
|     viewingAssetStoreState, | ||||
|   } from '$lib/stores/asset-interaction.store'; | ||||
|   import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store'; | ||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||
|   import { locale } from '$lib/stores/preferences.store'; | ||||
|   import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; | ||||
|   import type { UserResponseDto } from '@api'; | ||||
| @@ -31,11 +22,20 @@ | ||||
|   import { browser } from '$app/environment'; | ||||
|   import { isSearchEnabled } from '$lib/stores/search.store'; | ||||
|   import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; | ||||
|   import type { AssetStore } from '$lib/stores/assets.store'; | ||||
|   import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; | ||||
|  | ||||
|   export let user: UserResponseDto | undefined = undefined; | ||||
|   export let isAlbumSelectionMode = false; | ||||
|   export let showMemoryLane = false; | ||||
|  | ||||
|   export let assetStore: AssetStore; | ||||
|   export let assetInteractionStore: AssetInteractionStore; | ||||
|  | ||||
|   const { assetSelectionCandidates, assetSelectionStart, selectedAssets, isMultiSelectState } = assetInteractionStore; | ||||
|  | ||||
|   let { isViewing: showAssetViewer, asset: viewingAsset } = assetViewingStore; | ||||
|  | ||||
|   let viewportHeight = 0; | ||||
|   let viewportWidth = 0; | ||||
|   let assetGridElement: HTMLElement; | ||||
| @@ -61,7 +61,7 @@ | ||||
|     // Get asset bucket if bucket height is smaller than viewport height | ||||
|     let bucketsToFetchInitially: string[] = []; | ||||
|     let initialBucketsHeight = 0; | ||||
|     $assetGridState.buckets.every((bucket) => { | ||||
|     $assetStore.buckets.every((bucket) => { | ||||
|       if (initialBucketsHeight < viewportHeight) { | ||||
|         initialBucketsHeight += bucket.bucketHeight; | ||||
|         bucketsToFetchInitially.push(bucket.bucketDate); | ||||
| @@ -89,7 +89,7 @@ | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (!$isViewingAssetStoreState) { | ||||
|     if (!$showAssetViewer) { | ||||
|       switch (event.key) { | ||||
|         case 'Escape': | ||||
|           assetInteractionStore.clearMultiselect(); | ||||
| @@ -121,12 +121,18 @@ | ||||
|     assetGridElement.scrollBy(0, event.detail.heightDelta); | ||||
|   } | ||||
|  | ||||
|   const navigateToPreviousAsset = () => { | ||||
|     assetInteractionStore.navigateAsset('previous'); | ||||
|   const navigateToPreviousAsset = async () => { | ||||
|     const prevAsset = await assetStore.getAdjacentAsset($viewingAsset.id, 'previous'); | ||||
|     if (prevAsset) { | ||||
|       assetViewingStore.setAssetId(prevAsset); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const navigateToNextAsset = () => { | ||||
|     assetInteractionStore.navigateAsset('next'); | ||||
|   const navigateToNextAsset = async () => { | ||||
|     const nextAsset = await assetStore.getAdjacentAsset($viewingAsset.id, 'next'); | ||||
|     if (nextAsset) { | ||||
|       assetViewingStore.setAssetId(nextAsset); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   let lastScrollPosition = 0; | ||||
| @@ -228,8 +234,8 @@ | ||||
|     assetInteractionStore.clearAssetSelectionCandidates(); | ||||
|  | ||||
|     if ($assetSelectionStart && rangeSelection) { | ||||
|       let startBucketIndex = $assetGridState.loadedAssets[$assetSelectionStart.id]; | ||||
|       let endBucketIndex = $assetGridState.loadedAssets[asset.id]; | ||||
|       let startBucketIndex = $assetStore.loadedAssets[$assetSelectionStart.id]; | ||||
|       let endBucketIndex = $assetStore.loadedAssets[asset.id]; | ||||
|  | ||||
|       if (endBucketIndex < startBucketIndex) { | ||||
|         [startBucketIndex, endBucketIndex] = [endBucketIndex, startBucketIndex]; | ||||
| @@ -237,7 +243,7 @@ | ||||
|  | ||||
|       // Select/deselect assets in all intermediate buckets | ||||
|       for (let bucketIndex = startBucketIndex + 1; bucketIndex < endBucketIndex; bucketIndex++) { | ||||
|         const bucket = $assetGridState.buckets[bucketIndex]; | ||||
|         const bucket = $assetStore.buckets[bucketIndex]; | ||||
|         await assetStore.getAssetsByBucket(bucket.bucketDate, BucketPosition.Unknown); | ||||
|         for (const asset of bucket.assets) { | ||||
|           if (deselect) { | ||||
| @@ -250,7 +256,7 @@ | ||||
|  | ||||
|       // Update date group selection | ||||
|       for (let bucketIndex = startBucketIndex; bucketIndex <= endBucketIndex; bucketIndex++) { | ||||
|         const bucket = $assetGridState.buckets[bucketIndex]; | ||||
|         const bucket = $assetStore.buckets[bucketIndex]; | ||||
|  | ||||
|         // Split bucket into date groups and check each group | ||||
|         const assetsGroupByDate = splitBucketIntoDateGroups(bucket.assets, $locale); | ||||
| @@ -279,18 +285,18 @@ | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     let start = $assetGridState.assets.indexOf(rangeStart); | ||||
|     let end = $assetGridState.assets.indexOf(asset); | ||||
|     let start = $assetStore.assets.indexOf(rangeStart); | ||||
|     let end = $assetStore.assets.indexOf(asset); | ||||
|  | ||||
|     if (start > end) { | ||||
|       [start, end] = [end, start]; | ||||
|     } | ||||
|  | ||||
|     assetInteractionStore.setAssetSelectionCandidates($assetGridState.assets.slice(start, end + 1)); | ||||
|     assetInteractionStore.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1)); | ||||
|   }; | ||||
|  | ||||
|   const onSelectStart = (e: Event) => { | ||||
|     if ($isMultiSelectStoreState && shiftKeyIsDown) { | ||||
|     if ($isMultiSelectState && shiftKeyIsDown) { | ||||
|       e.preventDefault(); | ||||
|     } | ||||
|   }; | ||||
| @@ -302,8 +308,9 @@ | ||||
|   <ShowShortcuts on:close={() => (showShortcuts = !showShortcuts)} /> | ||||
| {/if} | ||||
|  | ||||
| {#if bucketInfo && viewportHeight && $assetGridState.timelineHeight > viewportHeight} | ||||
| {#if bucketInfo && viewportHeight && $assetStore.timelineHeight > viewportHeight} | ||||
|   <Scrollbar | ||||
|     {assetStore} | ||||
|     scrollbarHeight={viewportHeight} | ||||
|     scrollTop={lastScrollPosition} | ||||
|     on:onscrollbarclick={(e) => handleScrollbarClick(e.detail)} | ||||
| @@ -324,15 +331,12 @@ | ||||
|     {#if showMemoryLane} | ||||
|       <MemoryLane /> | ||||
|     {/if} | ||||
|     <section id="virtual-timeline" style:height={$assetGridState.timelineHeight + 'px'}> | ||||
|       {#each $assetGridState.buckets as bucket, bucketIndex (bucketIndex)} | ||||
|     <section id="virtual-timeline" style:height={$assetStore.timelineHeight + 'px'}> | ||||
|       {#each $assetStore.buckets as bucket, bucketIndex (bucketIndex)} | ||||
|         <IntersectionObserver | ||||
|           on:intersected={intersectedHandler} | ||||
|           on:hidden={async () => { | ||||
|             // If bucket is hidden and in loading state, cancel the request | ||||
|             if ($loadingBucketState[bucket.bucketDate]) { | ||||
|               await assetStore.cancelBucketRequest(bucket.cancelToken, bucket.bucketDate); | ||||
|             } | ||||
|             await assetStore.cancelBucketRequest(bucket.cancelToken, bucket.bucketDate); | ||||
|           }} | ||||
|           let:intersecting | ||||
|           top={750} | ||||
| @@ -342,6 +346,8 @@ | ||||
|           <div id={'bucket_' + bucket.bucketDate} style:height={bucket.bucketHeight + 'px'}> | ||||
|             {#if intersecting} | ||||
|               <AssetDateGroup | ||||
|                 {assetStore} | ||||
|                 {assetInteractionStore} | ||||
|                 {isAlbumSelectionMode} | ||||
|                 on:shift={handleScrollTimeline} | ||||
|                 on:selectAssetCandidates={handleSelectAssetCandidates} | ||||
| @@ -360,13 +366,14 @@ | ||||
| </section> | ||||
|  | ||||
| <Portal target="body"> | ||||
|   {#if $isViewingAssetStoreState} | ||||
|   {#if $showAssetViewer} | ||||
|     <AssetViewer | ||||
|       asset={$viewingAssetStoreState} | ||||
|       {assetStore} | ||||
|       asset={$viewingAsset} | ||||
|       on:navigate-previous={navigateToPreviousAsset} | ||||
|       on:navigate-next={navigateToNextAsset} | ||||
|       on:close={() => { | ||||
|         assetInteractionStore.setIsViewingAsset(false); | ||||
|         assetViewingStore.showAssetViewer(false); | ||||
|       }} | ||||
|       on:archived={handleArchiveSuccess} | ||||
|     /> | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|   import { flip } from 'svelte/animate'; | ||||
|   import { archivedAsset } from '$lib/stores/archived-asset.store'; | ||||
|   import { getThumbnailSize } from '$lib/utils/thumbnail-util'; | ||||
|   import { isViewingAssetStoreState } from '$lib/stores/asset-interaction.store'; | ||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||
|  | ||||
|   export let assets: AssetResponseDto[]; | ||||
|   export let sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||
| @@ -20,6 +20,8 @@ | ||||
|   export let viewFrom: ViewFrom; | ||||
|   export let showArchiveIcon = false; | ||||
|  | ||||
|   let { isViewing: showAssetViewer } = assetViewingStore; | ||||
|  | ||||
|   let selectedAsset: AssetResponseDto; | ||||
|   let currentViewAssetIndex = 0; | ||||
|  | ||||
| @@ -33,7 +35,7 @@ | ||||
|  | ||||
|     currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id); | ||||
|     selectedAsset = assets[currentViewAssetIndex]; | ||||
|     $isViewingAssetStoreState = true; | ||||
|     $showAssetViewer = true; | ||||
|     pushState(selectedAsset.id); | ||||
|   }; | ||||
|  | ||||
| @@ -81,7 +83,7 @@ | ||||
|   }; | ||||
|  | ||||
|   const closeViewer = () => { | ||||
|     $isViewingAssetStoreState = false; | ||||
|     $showAssetViewer = false; | ||||
|     history.pushState(null, '', `${$page.url.pathname}`); | ||||
|   }; | ||||
|  | ||||
| @@ -117,7 +119,7 @@ | ||||
| {/if} | ||||
|  | ||||
| <!-- Overlay Asset Viewer --> | ||||
| {#if $isViewingAssetStoreState} | ||||
| {#if $showAssetViewer} | ||||
|   <AssetViewer | ||||
|     asset={selectedAsset} | ||||
|     publicSharedKey={sharedLink?.key} | ||||
|   | ||||
| @@ -19,15 +19,15 @@ | ||||
| <script lang="ts"> | ||||
|   import { albumAssetSelectionStore } from '$lib/stores/album-asset-selection.store'; | ||||
|  | ||||
|   import { assetGridState } from '$lib/stores/assets.store'; | ||||
|  | ||||
|   import { createEventDispatcher } from 'svelte'; | ||||
|   import { SegmentScrollbarLayout } from './segment-scrollbar-layout'; | ||||
|   import type { AssetStore } from '$lib/stores/assets.store'; | ||||
|  | ||||
|   export let scrollTop = 0; | ||||
|   export let scrollbarHeight = 0; | ||||
|   export let assetStore: AssetStore; | ||||
|  | ||||
|   $: timelineHeight = $assetGridState.timelineHeight; | ||||
|   $: timelineHeight = $assetStore.timelineHeight; | ||||
|   $: timelineScrolltop = (scrollbarPosition / scrollbarHeight) * timelineHeight; | ||||
|  | ||||
|   let segmentScrollbarLayout: SegmentScrollbarLayout[] = []; | ||||
| @@ -48,7 +48,7 @@ | ||||
|  | ||||
|   $: { | ||||
|     let result: SegmentScrollbarLayout[] = []; | ||||
|     for (const bucket of $assetGridState.buckets) { | ||||
|     for (const bucket of $assetStore.buckets) { | ||||
|       let segmentLayout = new SegmentScrollbarLayout(); | ||||
|       segmentLayout.count = bucket.assets.length; | ||||
|       segmentLayout.height = (bucket.bucketHeight / timelineHeight) * scrollbarHeight; | ||||
|   | ||||
| @@ -1,49 +1,68 @@ | ||||
| import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state'; | ||||
| import { api, AssetResponseDto } from '@api'; | ||||
| import { derived, writable } from 'svelte/store'; | ||||
| import { assetGridState, assetStore } from './assets.store'; | ||||
| import type { AssetResponseDto } from '../../api/open-api'; | ||||
|  | ||||
| // Asset Viewer | ||||
| export const viewingAssetStoreState = writable<AssetResponseDto>(); | ||||
| export const isViewingAssetStoreState = writable<boolean>(false); | ||||
| export interface AssetInteractionStore { | ||||
|   addAssetToMultiselectGroup: (asset: AssetResponseDto) => void; | ||||
|   removeAssetFromMultiselectGroup: (asset: AssetResponseDto) => void; | ||||
|   addGroupToMultiselectGroup: (group: string) => void; | ||||
|   removeGroupFromMultiselectGroup: (group: string) => void; | ||||
|   setAssetSelectionCandidates: (assets: AssetResponseDto[]) => void; | ||||
|   clearAssetSelectionCandidates: () => void; | ||||
|   setAssetSelectionStart: (asset: AssetResponseDto | null) => void; | ||||
|   clearMultiselect: () => void; | ||||
|   isMultiSelectState: { | ||||
|     subscribe: (run: (value: boolean) => void, invalidate?: (value?: boolean) => void) => () => void; | ||||
|   }; | ||||
|   assetsInAlbumState: { | ||||
|     subscribe: ( | ||||
|       run: (value: AssetResponseDto[]) => void, | ||||
|       invalidate?: (value?: AssetResponseDto[]) => void, | ||||
|     ) => () => void; | ||||
|     set: (value: AssetResponseDto[]) => void; | ||||
|   }; | ||||
|   selectedAssets: { | ||||
|     subscribe: ( | ||||
|       run: (value: Set<AssetResponseDto>) => void, | ||||
|       invalidate?: (value?: Set<AssetResponseDto>) => void, | ||||
|     ) => () => void; | ||||
|   }; | ||||
|   selectedGroup: { | ||||
|     subscribe: (run: (value: Set<string>) => void, invalidate?: (value?: Set<string>) => void) => () => void; | ||||
|   }; | ||||
|   assetSelectionCandidates: { | ||||
|     subscribe: ( | ||||
|       run: (value: Set<AssetResponseDto>) => void, | ||||
|       invalidate?: (value?: Set<AssetResponseDto>) => void, | ||||
|     ) => () => void; | ||||
|   }; | ||||
|   assetSelectionStart: { | ||||
|     subscribe: ( | ||||
|       run: (value: AssetResponseDto | null) => void, | ||||
|       invalidate?: (value?: AssetResponseDto | null) => void, | ||||
|     ) => () => void; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Multi-selection mode | ||||
|  */ | ||||
| export const assetsInAlbumStoreState = writable<AssetResponseDto[]>([]); | ||||
| // Selected assets | ||||
| export const selectedAssets = writable<Set<AssetResponseDto>>(new Set()); | ||||
| // Selected date groups | ||||
| export const selectedGroup = writable<Set<string>>(new Set()); | ||||
| // If any asset selected | ||||
| export const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0); | ||||
|  | ||||
| /** | ||||
|  * Range selection | ||||
|  */ | ||||
| // Candidates for the range selection. This set includes only loaded assets, so it improves highlight | ||||
| // performance. From the user's perspective, range is highlighted almost immediately | ||||
| export const assetSelectionCandidates = writable<Set<AssetResponseDto>>(new Set()); | ||||
| // The beginning of the selection range | ||||
| export const assetSelectionStart = writable<AssetResponseDto | null>(null); | ||||
|  | ||||
| function createAssetInteractionStore() { | ||||
|   let _assetGridState = new AssetGridState(); | ||||
|   let _viewingAssetStoreState: AssetResponseDto; | ||||
| export function createAssetInteractionStore(): AssetInteractionStore { | ||||
|   let _selectedAssets: Set<AssetResponseDto>; | ||||
|   let _selectedGroup: Set<string>; | ||||
|   let _assetsInAlbums: AssetResponseDto[]; | ||||
|   let _assetSelectionCandidates: Set<AssetResponseDto>; | ||||
|   let _assetSelectionStart: AssetResponseDto | null; | ||||
|  | ||||
|   // Subscriber | ||||
|   assetGridState.subscribe((state) => { | ||||
|     _assetGridState = state; | ||||
|   }); | ||||
|   const assetsInAlbumStoreState = writable<AssetResponseDto[]>([]); | ||||
|   // Selected assets | ||||
|   const selectedAssets = writable<Set<AssetResponseDto>>(new Set()); | ||||
|   // Selected date groups | ||||
|   const selectedGroup = writable<Set<string>>(new Set()); | ||||
|   // If any asset selected | ||||
|   const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0); | ||||
|  | ||||
|   viewingAssetStoreState.subscribe((asset) => { | ||||
|     _viewingAssetStoreState = asset; | ||||
|   }); | ||||
|   // Candidates for the range selection. This set includes only loaded assets, so it improves highlight | ||||
|   // performance. From the user's perspective, range is highlighted almost immediately | ||||
|   const assetSelectionCandidates = writable<Set<AssetResponseDto>>(new Set()); | ||||
|   // The beginning of the selection range | ||||
|   const assetSelectionStart = writable<AssetResponseDto | null>(null); | ||||
|  | ||||
|   selectedAssets.subscribe((assets) => { | ||||
|     _selectedAssets = assets; | ||||
| @@ -64,89 +83,7 @@ function createAssetInteractionStore() { | ||||
|   assetSelectionStart.subscribe((asset) => { | ||||
|     _assetSelectionStart = asset; | ||||
|   }); | ||||
|   // Methods | ||||
|  | ||||
|   /** | ||||
|    * Asset Viewer | ||||
|    */ | ||||
|   const setViewingAsset = async (asset: AssetResponseDto) => { | ||||
|     setViewingAssetId(asset.id); | ||||
|   }; | ||||
|  | ||||
|   const setViewingAssetId = async (id: string) => { | ||||
|     const { data } = await api.assetApi.getAssetById({ id }); | ||||
|     viewingAssetStoreState.set(data); | ||||
|     isViewingAssetStoreState.set(true); | ||||
|   }; | ||||
|  | ||||
|   const setIsViewingAsset = (isViewing: boolean) => { | ||||
|     isViewingAssetStoreState.set(isViewing); | ||||
|   }; | ||||
|  | ||||
|   const getNextAsset = async (currentBucketIndex: number, assetId: string): Promise<AssetResponseDto | null> => { | ||||
|     const currentBucket = _assetGridState.buckets[currentBucketIndex]; | ||||
|     const assetIndex = currentBucket.assets.findIndex(({ id }) => id == assetId); | ||||
|     if (assetIndex === -1) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     if (assetIndex + 1 < currentBucket.assets.length) { | ||||
|       return currentBucket.assets[assetIndex + 1]; | ||||
|     } | ||||
|  | ||||
|     const nextBucketIndex = currentBucketIndex + 1; | ||||
|     if (nextBucketIndex >= _assetGridState.buckets.length) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     const nextBucket = _assetGridState.buckets[nextBucketIndex]; | ||||
|     await assetStore.getAssetsByBucket(nextBucket.bucketDate, BucketPosition.Unknown); | ||||
|  | ||||
|     return nextBucket.assets[0] ?? null; | ||||
|   }; | ||||
|  | ||||
|   const getPrevAsset = async (currentBucketIndex: number, assetId: string): Promise<AssetResponseDto | null> => { | ||||
|     const currentBucket = _assetGridState.buckets[currentBucketIndex]; | ||||
|     const assetIndex = currentBucket.assets.findIndex(({ id }) => id == assetId); | ||||
|     if (assetIndex === -1) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     if (assetIndex > 0) { | ||||
|       return currentBucket.assets[assetIndex - 1]; | ||||
|     } | ||||
|  | ||||
|     const prevBucketIndex = currentBucketIndex - 1; | ||||
|     if (prevBucketIndex < 0) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     const prevBucket = _assetGridState.buckets[prevBucketIndex]; | ||||
|     await assetStore.getAssetsByBucket(prevBucket.bucketDate, BucketPosition.Unknown); | ||||
|  | ||||
|     return prevBucket.assets[prevBucket.assets.length - 1] ?? null; | ||||
|   }; | ||||
|  | ||||
|   const navigateAsset = async (direction: 'next' | 'previous') => { | ||||
|     const currentAssetId = _viewingAssetStoreState.id; | ||||
|     const currentBucketIndex = _assetGridState.loadedAssets[currentAssetId]; | ||||
|     if (currentBucketIndex < 0 || currentBucketIndex >= _assetGridState.buckets.length) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const asset = | ||||
|       direction === 'next' | ||||
|         ? await getNextAsset(currentBucketIndex, currentAssetId) | ||||
|         : await getPrevAsset(currentBucketIndex, currentAssetId); | ||||
|  | ||||
|     if (asset) { | ||||
|       setViewingAsset(asset); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * Multiselect | ||||
|    */ | ||||
|   const addAssetToMultiselectGroup = (asset: AssetResponseDto) => { | ||||
|     // Not select if in album already | ||||
|     if (_assetsInAlbums.find((a) => a.id === asset.id)) { | ||||
| @@ -205,10 +142,6 @@ function createAssetInteractionStore() { | ||||
|   }; | ||||
|  | ||||
|   return { | ||||
|     setViewingAsset, | ||||
|     setViewingAssetId, | ||||
|     setIsViewingAsset, | ||||
|     navigateAsset, | ||||
|     addAssetToMultiselectGroup, | ||||
|     removeAssetFromMultiselectGroup, | ||||
|     addGroupToMultiselectGroup, | ||||
| @@ -217,7 +150,24 @@ function createAssetInteractionStore() { | ||||
|     clearAssetSelectionCandidates, | ||||
|     setAssetSelectionStart, | ||||
|     clearMultiselect, | ||||
|     isMultiSelectState: { | ||||
|       subscribe: isMultiSelectStoreState.subscribe, | ||||
|     }, | ||||
|     assetsInAlbumState: { | ||||
|       subscribe: assetsInAlbumStoreState.subscribe, | ||||
|       set: assetsInAlbumStoreState.set, | ||||
|     }, | ||||
|     selectedAssets: { | ||||
|       subscribe: selectedAssets.subscribe, | ||||
|     }, | ||||
|     selectedGroup: { | ||||
|       subscribe: selectedGroup.subscribe, | ||||
|     }, | ||||
|     assetSelectionCandidates: { | ||||
|       subscribe: assetSelectionCandidates.subscribe, | ||||
|     }, | ||||
|     assetSelectionStart: { | ||||
|       subscribe: assetSelectionStart.subscribe, | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export const assetInteractionStore = createAssetInteractionStore(); | ||||
|   | ||||
							
								
								
									
										31
									
								
								web/src/lib/stores/asset-viewing.store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								web/src/lib/stores/asset-viewing.store.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { writable } from 'svelte/store'; | ||||
| import { api, type AssetResponseDto } from '@api'; | ||||
|  | ||||
| function createAssetViewingStore() { | ||||
|   const viewingAssetStoreState = writable<AssetResponseDto>(); | ||||
|   const viewState = writable<boolean>(false); | ||||
|  | ||||
|   const setAssetId = async (id: string) => { | ||||
|     const { data } = await api.assetApi.getAssetById({ id }); | ||||
|     viewingAssetStoreState.set(data); | ||||
|     viewState.set(true); | ||||
|   }; | ||||
|  | ||||
|   const showAssetViewer = (show: boolean) => { | ||||
|     viewState.set(show); | ||||
|   }; | ||||
|  | ||||
|   return { | ||||
|     asset: { | ||||
|       subscribe: viewingAssetStoreState.subscribe, | ||||
|     }, | ||||
|     isViewing: { | ||||
|       subscribe: viewState.subscribe, | ||||
|       set: viewState.set, | ||||
|     }, | ||||
|     setAssetId, | ||||
|     showAssetViewer, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export const assetViewingStore = createAssetViewingStore(); | ||||
| @@ -1,25 +1,34 @@ | ||||
| import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state'; | ||||
| import { api, AssetCountByTimeBucketResponseDto } from '@api'; | ||||
| import { api, AssetCountByTimeBucketResponseDto, AssetResponseDto } from '@api'; | ||||
| import { writable } from 'svelte/store'; | ||||
|  | ||||
| /** | ||||
|  * The state that holds information about the asset grid | ||||
|  */ | ||||
| export const assetGridState = writable<AssetGridState>(new AssetGridState()); | ||||
| export const loadingBucketState = writable<{ [key: string]: boolean }>({}); | ||||
| export interface AssetStore { | ||||
|   setInitialState: ( | ||||
|     viewportHeight: number, | ||||
|     viewportWidth: number, | ||||
|     data: AssetCountByTimeBucketResponseDto, | ||||
|     userId: string | undefined, | ||||
|   ) => void; | ||||
|   getAssetsByBucket: (bucket: string, position: BucketPosition) => Promise<void>; | ||||
|   updateBucketHeight: (bucket: string, actualBucketHeight: number) => number; | ||||
|   cancelBucketRequest: (token: AbortController, bucketDate: string) => Promise<void>; | ||||
|   getAdjacentAsset: (assetId: string, direction: 'next' | 'previous') => Promise<string | null>; | ||||
|   removeAsset: (assetId: string) => void; | ||||
|   updateAsset: (assetId: string, isFavorite: boolean) => void; | ||||
|   subscribe: (run: (value: AssetGridState) => void, invalidate?: (value?: AssetGridState) => void) => () => void; | ||||
| } | ||||
|  | ||||
| function createAssetStore() { | ||||
| export function createAssetStore(): AssetStore { | ||||
|   let _loadingBuckets: { [key: string]: boolean } = {}; | ||||
|   let _assetGridState = new AssetGridState(); | ||||
|   assetGridState.subscribe((state) => { | ||||
|  | ||||
|   const { subscribe, set, update } = writable(new AssetGridState()); | ||||
|  | ||||
|   subscribe((state) => { | ||||
|     _assetGridState = state; | ||||
|   }); | ||||
|  | ||||
|   let _loadingBucketState: { [key: string]: boolean } = {}; | ||||
|   loadingBucketState.subscribe((state) => { | ||||
|     _loadingBucketState = state; | ||||
|   }); | ||||
|  | ||||
|   const estimateViewportHeight = (assetCount: number, viewportWidth: number): number => { | ||||
|   const _estimateViewportHeight = (assetCount: number, viewportWidth: number): number => { | ||||
|     // Ideally we would use the average aspect ratio for the photoset, however assume | ||||
|     // a normal landscape aspect ratio of 3:2, then discount for the likelihood we | ||||
|     // will be scaling down and coalescing. | ||||
| @@ -39,25 +48,19 @@ function createAssetStore() { | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * Set initial state | ||||
|    * @param viewportHeight | ||||
|    * @param viewportWidth | ||||
|    * @param data | ||||
|    */ | ||||
|   const setInitialState = ( | ||||
|     viewportHeight: number, | ||||
|     viewportWidth: number, | ||||
|     data: AssetCountByTimeBucketResponseDto, | ||||
|     userId: string | undefined, | ||||
|   ) => { | ||||
|     assetGridState.set({ | ||||
|     set({ | ||||
|       viewportHeight, | ||||
|       viewportWidth, | ||||
|       timelineHeight: 0, | ||||
|       buckets: data.buckets.map((bucket) => ({ | ||||
|         bucketDate: bucket.timeBucket, | ||||
|         bucketHeight: estimateViewportHeight(bucket.count, viewportWidth), | ||||
|         bucketHeight: _estimateViewportHeight(bucket.count, viewportWidth), | ||||
|         assets: [], | ||||
|         cancelToken: new AbortController(), | ||||
|         position: BucketPosition.Unknown, | ||||
| @@ -67,8 +70,7 @@ function createAssetStore() { | ||||
|       userId, | ||||
|     }); | ||||
|  | ||||
|     // Update timeline height based on calculated bucket height | ||||
|     assetGridState.update((state) => { | ||||
|     update((state) => { | ||||
|       state.timelineHeight = state.buckets.reduce((acc, b) => acc + b.bucketHeight, 0); | ||||
|       return state; | ||||
|     }); | ||||
| @@ -78,7 +80,7 @@ function createAssetStore() { | ||||
|     try { | ||||
|       const currentBucketData = _assetGridState.buckets.find((b) => b.bucketDate === bucket); | ||||
|       if (currentBucketData?.assets && currentBucketData.assets.length > 0) { | ||||
|         assetGridState.update((state) => { | ||||
|         update((state) => { | ||||
|           const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket); | ||||
|           state.buckets[bucketIndex].position = position; | ||||
|           return state; | ||||
| @@ -86,10 +88,7 @@ function createAssetStore() { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       loadingBucketState.set({ | ||||
|         ..._loadingBucketState, | ||||
|         [bucket]: true, | ||||
|       }); | ||||
|       _loadingBuckets = { ..._loadingBuckets, [bucket]: true }; | ||||
|       const { data: assets } = await api.assetApi.getAssetByTimeBucket( | ||||
|         { | ||||
|           getAssetByTimeBucketDto: { | ||||
| @@ -100,13 +99,9 @@ function createAssetStore() { | ||||
|         }, | ||||
|         { signal: currentBucketData?.cancelToken.signal }, | ||||
|       ); | ||||
|       loadingBucketState.set({ | ||||
|         ..._loadingBucketState, | ||||
|         [bucket]: false, | ||||
|       }); | ||||
|       _loadingBuckets = { ..._loadingBuckets, [bucket]: false }; | ||||
|  | ||||
|       // Update assetGridState with assets by time bucket | ||||
|       assetGridState.update((state) => { | ||||
|       update((state) => { | ||||
|         const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket); | ||||
|         state.buckets[bucketIndex].assets = assets; | ||||
|         state.buckets[bucketIndex].position = position; | ||||
| @@ -125,7 +120,7 @@ function createAssetStore() { | ||||
|   }; | ||||
|  | ||||
|   const removeAsset = (assetId: string) => { | ||||
|     assetGridState.update((state) => { | ||||
|     update((state) => { | ||||
|       const bucketIndex = state.buckets.findIndex((b) => b.assets.some((a) => a.id === assetId)); | ||||
|       const assetIndex = state.buckets[bucketIndex].assets.findIndex((a) => a.id === assetId); | ||||
|       state.buckets[bucketIndex].assets.splice(assetIndex, 1); | ||||
| @@ -140,7 +135,7 @@ function createAssetStore() { | ||||
|   }; | ||||
|  | ||||
|   const _removeBucket = (bucketDate: string) => { | ||||
|     assetGridState.update((state) => { | ||||
|     update((state) => { | ||||
|       const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucketDate); | ||||
|       state.buckets.splice(bucketIndex, 1); | ||||
|       state.assets = state.buckets.flatMap((b) => b.assets); | ||||
| @@ -153,7 +148,7 @@ function createAssetStore() { | ||||
|     let scrollTimeline = false; | ||||
|     let heightDelta = 0; | ||||
|  | ||||
|     assetGridState.update((state) => { | ||||
|     update((state) => { | ||||
|       const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket); | ||||
|       // Update timeline height based on the new bucket height | ||||
|       const estimateBucketHeight = state.buckets[bucketIndex].bucketHeight; | ||||
| @@ -177,9 +172,13 @@ function createAssetStore() { | ||||
|   }; | ||||
|  | ||||
|   const cancelBucketRequest = async (token: AbortController, bucketDate: string) => { | ||||
|     if (!_loadingBuckets[bucketDate]) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     token.abort(); | ||||
|     // set new abort controller for bucket | ||||
|     assetGridState.update((state) => { | ||||
|  | ||||
|     update((state) => { | ||||
|       const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucketDate); | ||||
|       state.buckets[bucketIndex].cancelToken = new AbortController(); | ||||
|       return state; | ||||
| @@ -187,7 +186,7 @@ function createAssetStore() { | ||||
|   }; | ||||
|  | ||||
|   const updateAsset = (assetId: string, isFavorite: boolean) => { | ||||
|     assetGridState.update((state) => { | ||||
|     update((state) => { | ||||
|       const bucketIndex = state.buckets.findIndex((b) => b.assets.some((a) => a.id === assetId)); | ||||
|       const assetIndex = state.buckets[bucketIndex].assets.findIndex((a) => a.id === assetId); | ||||
|       state.buckets[bucketIndex].assets[assetIndex].isFavorite = isFavorite; | ||||
| @@ -198,14 +197,72 @@ function createAssetStore() { | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   const _getNextAsset = async (currentBucketIndex: number, assetId: string): Promise<AssetResponseDto | null> => { | ||||
|     const currentBucket = _assetGridState.buckets[currentBucketIndex]; | ||||
|     const assetIndex = currentBucket.assets.findIndex(({ id }) => id == assetId); | ||||
|     if (assetIndex === -1) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     if (assetIndex + 1 < currentBucket.assets.length) { | ||||
|       return currentBucket.assets[assetIndex + 1]; | ||||
|     } | ||||
|  | ||||
|     const nextBucketIndex = currentBucketIndex + 1; | ||||
|     if (nextBucketIndex >= _assetGridState.buckets.length) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     const nextBucket = _assetGridState.buckets[nextBucketIndex]; | ||||
|     await getAssetsByBucket(nextBucket.bucketDate, BucketPosition.Unknown); | ||||
|  | ||||
|     return nextBucket.assets[0] ?? null; | ||||
|   }; | ||||
|  | ||||
|   const _getPrevAsset = async (currentBucketIndex: number, assetId: string): Promise<AssetResponseDto | null> => { | ||||
|     const currentBucket = _assetGridState.buckets[currentBucketIndex]; | ||||
|     const assetIndex = currentBucket.assets.findIndex(({ id }) => id == assetId); | ||||
|     if (assetIndex === -1) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     if (assetIndex > 0) { | ||||
|       return currentBucket.assets[assetIndex - 1]; | ||||
|     } | ||||
|  | ||||
|     const prevBucketIndex = currentBucketIndex - 1; | ||||
|     if (prevBucketIndex < 0) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     const prevBucket = _assetGridState.buckets[prevBucketIndex]; | ||||
|     await getAssetsByBucket(prevBucket.bucketDate, BucketPosition.Unknown); | ||||
|  | ||||
|     return prevBucket.assets[prevBucket.assets.length - 1] ?? null; | ||||
|   }; | ||||
|  | ||||
|   const getAdjacentAsset = async (assetId: string, direction: 'next' | 'previous'): Promise<string | null> => { | ||||
|     const currentBucketIndex = _assetGridState.loadedAssets[assetId]; | ||||
|     if (currentBucketIndex < 0 || currentBucketIndex >= _assetGridState.buckets.length) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     const asset = | ||||
|       direction === 'next' | ||||
|         ? await _getNextAsset(currentBucketIndex, assetId) | ||||
|         : await _getPrevAsset(currentBucketIndex, assetId); | ||||
|  | ||||
|     return asset?.id ?? null; | ||||
|   }; | ||||
|  | ||||
|   return { | ||||
|     setInitialState, | ||||
|     getAssetsByBucket, | ||||
|     removeAsset, | ||||
|     updateBucketHeight, | ||||
|     cancelBucketRequest, | ||||
|     getAdjacentAsset, | ||||
|     updateAsset, | ||||
|     subscribe, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export const assetStore = createAssetStore(); | ||||
|   | ||||
| @@ -3,11 +3,6 @@ | ||||
|   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; | ||||
|   import MapSettingsModal from '$lib/components/map-page/map-settings-modal.svelte'; | ||||
|   import Portal from '$lib/components/shared-components/portal/portal.svelte'; | ||||
|   import { | ||||
|     assetInteractionStore, | ||||
|     isViewingAssetStoreState, | ||||
|     viewingAssetStoreState, | ||||
|   } from '$lib/stores/asset-interaction.store'; | ||||
|   import { mapSettings } from '$lib/stores/preferences.store'; | ||||
|   import { MapMarkerResponseDto, api } from '@api'; | ||||
|   import { isEqual, omit } from 'lodash-es'; | ||||
| @@ -15,9 +10,12 @@ | ||||
|   import Cog from 'svelte-material-icons/Cog.svelte'; | ||||
|   import type { PageData } from './$types'; | ||||
|   import { DateTime, Duration } from 'luxon'; | ||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||
|  | ||||
|   export let data: PageData; | ||||
|  | ||||
|   let { isViewing: showAssetViewer, asset: viewingAsset } = assetViewingStore; | ||||
|  | ||||
|   let leaflet: typeof import('$lib/components/shared-components/leaflet'); | ||||
|   let mapMarkers: MapMarkerResponseDto[] = []; | ||||
|   let abortController: AbortController; | ||||
| @@ -34,8 +32,7 @@ | ||||
|     if (abortController) { | ||||
|       abortController.abort(); | ||||
|     } | ||||
|     assetInteractionStore.clearMultiselect(); | ||||
|     assetInteractionStore.setIsViewingAsset(false); | ||||
|     assetViewingStore.showAssetViewer(false); | ||||
|   }); | ||||
|  | ||||
|   async function loadMapMarkers() { | ||||
| @@ -83,20 +80,20 @@ | ||||
|   } | ||||
|  | ||||
|   function onViewAssets(assetIds: string[], activeAssetIndex: number) { | ||||
|     assetInteractionStore.setViewingAssetId(assetIds[activeAssetIndex]); | ||||
|     assetViewingStore.setAssetId(assetIds[activeAssetIndex]); | ||||
|     viewingAssets = assetIds; | ||||
|     viewingAssetCursor = activeAssetIndex; | ||||
|   } | ||||
|  | ||||
|   function navigateNext() { | ||||
|     if (viewingAssetCursor < viewingAssets.length - 1) { | ||||
|       assetInteractionStore.setViewingAssetId(viewingAssets[++viewingAssetCursor]); | ||||
|       assetViewingStore.setAssetId(viewingAssets[++viewingAssetCursor]); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function navigatePrevious() { | ||||
|     if (viewingAssetCursor > 0) { | ||||
|       assetInteractionStore.setViewingAssetId(viewingAssets[--viewingAssetCursor]); | ||||
|       assetViewingStore.setAssetId(viewingAssets[--viewingAssetCursor]); | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
| @@ -142,14 +139,14 @@ | ||||
| </UserPageLayout> | ||||
|  | ||||
| <Portal target="body"> | ||||
|   {#if $isViewingAssetStoreState} | ||||
|   {#if $showAssetViewer} | ||||
|     <AssetViewer | ||||
|       asset={$viewingAssetStoreState} | ||||
|       asset={$viewingAsset} | ||||
|       showNavigation={viewingAssets.length > 1} | ||||
|       on:navigate-next={navigateNext} | ||||
|       on:navigate-previous={navigatePrevious} | ||||
|       on:close={() => { | ||||
|         assetInteractionStore.setIsViewingAsset(false); | ||||
|         assetViewingStore.showAssetViewer(false); | ||||
|       }} | ||||
|     /> | ||||
|   {/if} | ||||
|   | ||||
| @@ -8,21 +8,26 @@ | ||||
|   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 { assetInteractionStore, isMultiSelectStoreState, selectedAssets } from '$lib/stores/asset-interaction.store'; | ||||
|   import { onDestroy } from 'svelte'; | ||||
|   import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; | ||||
|   import Plus from 'svelte-material-icons/Plus.svelte'; | ||||
|   import type { PageData } from './$types'; | ||||
|   import { createAssetStore } from '$lib/stores/assets.store'; | ||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; | ||||
|  | ||||
|   export let data: PageData; | ||||
|  | ||||
|   const assetStore = createAssetStore(); | ||||
|   const assetInteractionStore = createAssetInteractionStore(); | ||||
|   const { isMultiSelectState, selectedAssets } = assetInteractionStore; | ||||
|  | ||||
|   onDestroy(() => { | ||||
|     assetInteractionStore.clearMultiselect(); | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| <main class="grid h-screen bg-immich-bg pt-18 dark:bg-immich-dark-bg"> | ||||
|   {#if $isMultiSelectStoreState} | ||||
|   {#if $isMultiSelectState} | ||||
|     <AssetSelectControlBar assets={$selectedAssets} clearSelect={assetInteractionStore.clearMultiselect}> | ||||
|       <DownloadAction /> | ||||
|     </AssetSelectControlBar> | ||||
| @@ -44,5 +49,5 @@ | ||||
|       </svelte:fragment> | ||||
|     </ControlAppBar> | ||||
|   {/if} | ||||
|   <AssetGrid user={data.partner} /> | ||||
|   <AssetGrid {assetStore} {assetInteractionStore} user={data.partner} /> | ||||
| </main> | ||||
|   | ||||
| @@ -11,8 +11,8 @@ | ||||
|   import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; | ||||
|   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; | ||||
|   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; | ||||
|   import { assetInteractionStore, isMultiSelectStoreState, selectedAssets } from '$lib/stores/asset-interaction.store'; | ||||
|   import { assetStore } from '$lib/stores/assets.store'; | ||||
|   import { createAssetStore } from '$lib/stores/assets.store'; | ||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; | ||||
|   import { openFileUploadDialog } from '$lib/utils/file-uploader'; | ||||
|   import { api } from '@api'; | ||||
|   import { onDestroy, onMount } from 'svelte'; | ||||
| @@ -23,6 +23,10 @@ | ||||
|   export let data: PageData; | ||||
|   let assetCount = 1; | ||||
|  | ||||
|   const assetStore = createAssetStore(); | ||||
|   const assetInteractionStore = createAssetInteractionStore(); | ||||
|   const { isMultiSelectState, selectedAssets } = assetInteractionStore; | ||||
|  | ||||
|   onMount(async () => { | ||||
|     const { data: stats } = await api.assetApi.getAssetStats(); | ||||
|     assetCount = stats.total; | ||||
| @@ -39,12 +43,12 @@ | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| <UserPageLayout user={data.user} hideNavbar={$isMultiSelectStoreState} showUploadButton> | ||||
| <UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton> | ||||
|   <svelte:fragment slot="header"> | ||||
|     {#if $isMultiSelectStoreState} | ||||
|     {#if $isMultiSelectState} | ||||
|       <AssetSelectControlBar assets={$selectedAssets} clearSelect={assetInteractionStore.clearMultiselect}> | ||||
|         <CreateSharedLink /> | ||||
|         <SelectAllAssets /> | ||||
|         <SelectAllAssets {assetStore} {assetInteractionStore} /> | ||||
|         <AssetSelectContextMenu icon={Plus} title="Add"> | ||||
|           <AddToAlbum /> | ||||
|           <AddToAlbum shared /> | ||||
| @@ -60,7 +64,7 @@ | ||||
|   </svelte:fragment> | ||||
|   <svelte:fragment slot="content"> | ||||
|     {#if assetCount} | ||||
|       <AssetGrid showMemoryLane /> | ||||
|       <AssetGrid {assetStore} {assetInteractionStore} showMemoryLane /> | ||||
|     {:else} | ||||
|       <EmptyPlaceholder text="CLICK TO UPLOAD YOUR FIRST PHOTO" actionHandler={handleUpload} /> | ||||
|     {/if} | ||||
|   | ||||
| @@ -25,10 +25,12 @@ | ||||
|   import { flip } from 'svelte/animate'; | ||||
|   import { onDestroy, onMount } from 'svelte'; | ||||
|   import { browser } from '$app/environment'; | ||||
|   import { isViewingAssetStoreState } from '$lib/stores/asset-interaction.store'; | ||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||
|  | ||||
|   export let data: PageData; | ||||
|  | ||||
|   let { isViewing: showAssetViewer } = assetViewingStore; | ||||
|  | ||||
|   // The GalleryViewer pushes it's own history state, which causes weird | ||||
|   // behavior for history.back(). To prevent that we store the previous page | ||||
|   // manually and navigate back to that. | ||||
| @@ -48,7 +50,7 @@ | ||||
|   }); | ||||
|  | ||||
|   const handleKeyboardPress = (event: KeyboardEvent) => { | ||||
|     if (!$isViewingAssetStoreState) { | ||||
|     if (!$showAssetViewer) { | ||||
|       switch (event.key) { | ||||
|         case 'Escape': | ||||
|           goto(previousRoute); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user