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