瀏覽代碼

feat(web): select a range of assets (#3086)

The shift key can be held to select a range of assets.

Fixes: #2862
Thomas 2 年之前
父節點
當前提交
8fd4edb206

+ 28 - 10
web/src/lib/components/assets/thumbnail/thumbnail.svelte

@@ -3,14 +3,15 @@
   import { timeToSeconds } from '$lib/utils/time-to-seconds';
   import { timeToSeconds } from '$lib/utils/time-to-seconds';
   import { api, AssetResponseDto, AssetTypeEnum, ThumbnailFormat } from '@api';
   import { api, AssetResponseDto, AssetTypeEnum, ThumbnailFormat } from '@api';
   import { createEventDispatcher } from 'svelte';
   import { createEventDispatcher } from 'svelte';
+  import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
   import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
   import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
+  import Heart from 'svelte-material-icons/Heart.svelte';
+  import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte';
   import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte';
   import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte';
   import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
   import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
-  import Heart from 'svelte-material-icons/Heart.svelte';
-  import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
+  import { fade } from 'svelte/transition';
   import ImageThumbnail from './image-thumbnail.svelte';
   import ImageThumbnail from './image-thumbnail.svelte';
   import VideoThumbnail from './video-thumbnail.svelte';
   import VideoThumbnail from './video-thumbnail.svelte';
-  import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte';
 
 
   const dispatch = createEventDispatcher();
   const dispatch = createEventDispatcher();
 
 
@@ -21,6 +22,7 @@
   export let thumbnailHeight: number | undefined = undefined;
   export let thumbnailHeight: number | undefined = undefined;
   export let format: ThumbnailFormat = ThumbnailFormat.Webp;
   export let format: ThumbnailFormat = ThumbnailFormat.Webp;
   export let selected = false;
   export let selected = false;
+  export let selectionCandidate = false;
   export let disabled = false;
   export let disabled = false;
   export let readonly = false;
   export let readonly = false;
   export let publicSharedKey: string | undefined = undefined;
   export let publicSharedKey: string | undefined = undefined;
@@ -30,7 +32,7 @@
 
 
   $: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
   $: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
 
 
-  $: [width, height] = (() => {
+  $: [width, height] = ((): [number, number] => {
     if (thumbnailSize) {
     if (thumbnailSize) {
       return [thumbnailSize, thumbnailSize];
       return [thumbnailSize, thumbnailSize];
     }
     }
@@ -42,12 +44,19 @@
     return [235, 235];
     return [235, 235];
   })();
   })();
 
 
-  const thumbnailClickedHandler = () => {
+  const thumbnailClickedHandler = (e: Event) => {
     if (!disabled) {
     if (!disabled) {
+      e.preventDefault();
       dispatch('click', { asset });
       dispatch('click', { asset });
     }
     }
   };
   };
 
 
+  const thumbnailKeyDownHandler = (e: KeyboardEvent) => {
+    if (e.key === 'Enter') {
+      thumbnailClickedHandler(e);
+    }
+  };
+
   const onIconClickedHandler = (e: MouseEvent) => {
   const onIconClickedHandler = (e: MouseEvent) => {
     e.stopPropagation();
     e.stopPropagation();
     if (!disabled) {
     if (!disabled) {
@@ -68,21 +77,23 @@
     on:mouseenter={() => (mouseOver = true)}
     on:mouseenter={() => (mouseOver = true)}
     on:mouseleave={() => (mouseOver = false)}
     on:mouseleave={() => (mouseOver = false)}
     on:click={thumbnailClickedHandler}
     on:click={thumbnailClickedHandler}
-    on:keydown={thumbnailClickedHandler}
+    on:keydown={thumbnailKeyDownHandler}
   >
   >
     {#if intersecting}
     {#if intersecting}
       <div class="absolute w-full h-full z-20">
       <div class="absolute w-full h-full z-20">
         <!-- Select asset button  -->
         <!-- Select asset button  -->
-        {#if !readonly}
+        {#if !readonly && (mouseOver || selected || selectionCandidate)}
           <button
           <button
             on:click={onIconClickedHandler}
             on:click={onIconClickedHandler}
-            class="absolute p-2 group-hover:block"
-            class:group-hover:block={!disabled}
-            class:hidden={!selected}
+            on:keydown|preventDefault
+            on:keyup|preventDefault
+            class="absolute p-2"
             class:cursor-not-allowed={disabled}
             class:cursor-not-allowed={disabled}
             role="checkbox"
             role="checkbox"
             aria-checked={selected}
             aria-checked={selected}
             {disabled}
             {disabled}
+            in:fade={{ duration: 100 }}
+            out:fade={{ duration: 100 }}
           >
           >
             {#if disabled}
             {#if disabled}
               <CheckCircle size="24" class="text-zinc-800" />
               <CheckCircle size="24" class="text-zinc-800" />
@@ -153,6 +164,13 @@
           </div>
           </div>
         {/if}
         {/if}
       </div>
       </div>
+      {#if selectionCandidate}
+        <div
+          class="absolute w-full h-full top-0 bg-immich-primary opacity-40"
+          in:fade={{ duration: 100 }}
+          out:fade={{ duration: 100 }}
+        />
+      {/if}
     {/if}
     {/if}
   </div>
   </div>
 </IntersectionObserver>
 </IntersectionObserver>

+ 24 - 14
web/src/lib/components/photos-page/asset-date-group.svelte

@@ -1,6 +1,7 @@
 <script lang="ts">
 <script lang="ts">
   import {
   import {
     assetInteractionStore,
     assetInteractionStore,
+    assetSelectionCandidates,
     assetsInAlbumStoreState,
     assetsInAlbumStoreState,
     isMultiSelectStoreState,
     isMultiSelectStoreState,
     selectedAssets,
     selectedAssets,
@@ -8,15 +9,15 @@
   } from '$lib/stores/asset-interaction.store';
   } from '$lib/stores/asset-interaction.store';
   import { assetStore } from '$lib/stores/assets.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 type { AssetResponseDto } from '@api';
   import type { AssetResponseDto } from '@api';
   import justifiedLayout from 'justified-layout';
   import justifiedLayout from 'justified-layout';
   import lodash from 'lodash-es';
   import lodash from 'lodash-es';
+  import { createEventDispatcher } from 'svelte';
   import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
   import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
   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 { getAssetRatio } from '$lib/utils/asset-utils';
   import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
   import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
-  import { createEventDispatcher } from 'svelte';
 
 
   export let assets: AssetResponseDto[];
   export let assets: AssetResponseDto[];
   export let bucketDate: string;
   export let bucketDate: string;
@@ -130,18 +131,19 @@
     dateGroupTitle: string,
     dateGroupTitle: string,
   ) => {
   ) => {
     if ($selectedAssets.has(asset)) {
     if ($selectedAssets.has(asset)) {
+      for (const candidate of $assetSelectionCandidates || []) {
+        assetInteractionStore.removeAssetFromMultiselectGroup(candidate);
+      }
       assetInteractionStore.removeAssetFromMultiselectGroup(asset);
       assetInteractionStore.removeAssetFromMultiselectGroup(asset);
     } else {
     } else {
+      for (const candidate of $assetSelectionCandidates || []) {
+        assetInteractionStore.addAssetToMultiselectGroup(candidate);
+      }
       assetInteractionStore.addAssetToMultiselectGroup(asset);
       assetInteractionStore.addAssetToMultiselectGroup(asset);
     }
     }
 
 
     // Check if all assets are selected in a group to toggle the group selection's icon
     // Check if all assets are selected in a group to toggle the group selection's icon
-    let selectedAssetsInGroupCount = 0;
-    assetsInDateGroup.forEach((asset) => {
-      if ($selectedAssets.has(asset)) {
-        selectedAssetsInGroupCount++;
-      }
-    });
+    let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => $selectedAssets.has(asset)).length;
 
 
     // if all assets are selected in a group, add the group to selected group
     // if all assets are selected in a group, add the group to selected group
     if (selectedAssetsInGroupCount == assetsInDateGroup.length) {
     if (selectedAssetsInGroupCount == assetsInDateGroup.length) {
@@ -151,9 +153,13 @@
     }
     }
   };
   };
 
 
-  const assetMouseEventHandler = (dateGroupTitle: string) => {
+  const assetMouseEventHandler = (dateGroupTitle: string, asset: AssetResponseDto | null) => {
     // Show multi select icon on hover on date group
     // Show multi select icon on hover on date group
     hoveredDateGroup = dateGroupTitle;
     hoveredDateGroup = dateGroupTitle;
+
+    if ($isMultiSelectStoreState) {
+      dispatch('selectAssetCandidates', { asset });
+    }
   };
   };
 </script>
 </script>
 
 
@@ -171,9 +177,12 @@
       class="flex flex-col mt-5"
       class="flex flex-col mt-5"
       on:mouseenter={() => {
       on:mouseenter={() => {
         isMouseOverGroup = true;
         isMouseOverGroup = true;
-        assetMouseEventHandler(dateGroupTitle);
+        assetMouseEventHandler(dateGroupTitle, null);
+      }}
+      on:mouseleave={() => {
+        isMouseOverGroup = false;
+        assetMouseEventHandler(dateGroupTitle, null);
       }}
       }}
-      on:mouseleave={() => (isMouseOverGroup = false)}
     >
     >
       <!-- Date group title -->
       <!-- Date group title -->
       <p
       <p
@@ -216,9 +225,10 @@
               {groupIndex}
               {groupIndex}
               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)}
-              selected={$selectedAssets.has(asset) || $assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
-              disabled={$assetsInAlbumStoreState.findIndex((a) => a.id == asset.id) != -1}
+              on:mouse-event={() => assetMouseEventHandler(dateGroupTitle, asset)}
+              selected={$selectedAssets.has(asset) || $assetsInAlbumStoreState.some(({ id }) => id === asset.id)}
+              selectionCandidate={$assetSelectionCandidates.has(asset)}
+              disabled={$assetsInAlbumStoreState.some(({ id }) => id === asset.id)}
               thumbnailWidth={box.width}
               thumbnailWidth={box.width}
               thumbnailHeight={box.height}
               thumbnailHeight={box.height}
             />
             />

+ 77 - 2
web/src/lib/components/photos-page/asset-grid.svelte

@@ -1,12 +1,15 @@
 <script lang="ts">
 <script lang="ts">
+  import { BucketPosition } from '$lib/models/asset-grid-state';
   import {
   import {
     assetInteractionStore,
     assetInteractionStore,
+    isMultiSelectStoreState,
     isViewingAssetStoreState,
     isViewingAssetStoreState,
+    selectedAssets,
     viewingAssetStoreState,
     viewingAssetStoreState,
   } from '$lib/stores/asset-interaction.store';
   } from '$lib/stores/asset-interaction.store';
   import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
   import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
   import type { UserResponseDto } from '@api';
   import type { UserResponseDto } from '@api';
-  import { AssetCountByTimeBucketResponseDto, AssetResponseDto, TimeGroupEnum, api } from '@api';
+  import { api, AssetCountByTimeBucketResponseDto, AssetResponseDto, TimeGroupEnum } from '@api';
   import { onDestroy, onMount } from 'svelte';
   import { onDestroy, onMount } from 'svelte';
   import AssetViewer from '../asset-viewer/asset-viewer.svelte';
   import AssetViewer from '../asset-viewer/asset-viewer.svelte';
   import IntersectionObserver from '../asset-viewer/intersection-observer.svelte';
   import IntersectionObserver from '../asset-viewer/intersection-observer.svelte';
@@ -16,7 +19,6 @@
     OnScrollbarDragDetail,
     OnScrollbarDragDetail,
   } from '../shared-components/scrollbar/scrollbar.svelte';
   } from '../shared-components/scrollbar/scrollbar.svelte';
   import AssetDateGroup from './asset-date-group.svelte';
   import AssetDateGroup from './asset-date-group.svelte';
-  import { BucketPosition } from '$lib/models/asset-grid-state';
   import MemoryLane from './memory-lane.svelte';
   import MemoryLane from './memory-lane.svelte';
 
 
   export let user: UserResponseDto | undefined = undefined;
   export let user: UserResponseDto | undefined = undefined;
@@ -111,8 +113,80 @@
     navigateToNextAsset();
     navigateToNextAsset();
     assetStore.removeAsset(asset.id);
     assetStore.removeAsset(asset.id);
   };
   };
+
+  let lastAssetMouseEvent: AssetResponseDto | null = null;
+
+  $: if (!lastAssetMouseEvent) {
+    assetInteractionStore.clearAssetSelectionCandidates();
+  }
+
+  let shiftKeyIsDown = false;
+
+  const onKeyDown = (e: KeyboardEvent) => {
+    if (e.key === 'Shift') {
+      e.preventDefault();
+      shiftKeyIsDown = true;
+    }
+  };
+
+  const onKeyUp = (e: KeyboardEvent) => {
+    if (e.key === 'Shift') {
+      e.preventDefault();
+      shiftKeyIsDown = false;
+    }
+  };
+
+  $: if (!shiftKeyIsDown) {
+    assetInteractionStore.clearAssetSelectionCandidates();
+  }
+
+  $: if (shiftKeyIsDown && lastAssetMouseEvent) {
+    selectAssetCandidates(lastAssetMouseEvent);
+  }
+
+  const getLastSelectedAsset = () => {
+    let value;
+    for (value of $selectedAssets);
+    return value;
+  };
+
+  const handleSelectAssetCandidates = (e: CustomEvent) => {
+    const asset = e.detail.asset;
+    if (asset) {
+      selectAssetCandidates(asset);
+    }
+    lastAssetMouseEvent = asset;
+  };
+
+  const selectAssetCandidates = (asset: AssetResponseDto) => {
+    if (!shiftKeyIsDown) {
+      return;
+    }
+
+    const lastSelectedAsset = getLastSelectedAsset();
+    if (!lastSelectedAsset) {
+      return;
+    }
+
+    let start = $assetGridState.assets.indexOf(asset);
+    let end = $assetGridState.assets.indexOf(lastSelectedAsset);
+
+    if (start > end) {
+      [start, end] = [end, start];
+    }
+
+    assetInteractionStore.setAssetSelectionCandidates($assetGridState.assets.slice(start, end + 1));
+  };
+
+  const onSelectStart = (e: Event) => {
+    if ($isMultiSelectStoreState && shiftKeyIsDown) {
+      e.preventDefault();
+    }
+  };
 </script>
 </script>
 
 
+<svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} on:selectstart={onSelectStart} />
+
 {#if bucketInfo && viewportHeight && $assetGridState.timelineHeight > viewportHeight}
 {#if bucketInfo && viewportHeight && $assetGridState.timelineHeight > viewportHeight}
   <Scrollbar
   <Scrollbar
     scrollbarHeight={viewportHeight}
     scrollbarHeight={viewportHeight}
@@ -155,6 +229,7 @@
               <AssetDateGroup
               <AssetDateGroup
                 {isAlbumSelectionMode}
                 {isAlbumSelectionMode}
                 on:shift={handleScrollTimeline}
                 on:shift={handleScrollTimeline}
+                on:selectAssetCandidates={handleSelectAssetCandidates}
                 assets={bucket.assets}
                 assets={bucket.assets}
                 bucketDate={bucket.bucketDate}
                 bucketDate={bucket.bucketDate}
                 bucketHeight={bucket.bucketHeight}
                 bucketHeight={bucket.bucketHeight}

+ 20 - 0
web/src/lib/stores/asset-interaction.store.ts

@@ -12,6 +12,7 @@ export const assetsInAlbumStoreState = writable<AssetResponseDto[]>([]);
 export const selectedAssets = writable<Set<AssetResponseDto>>(new Set());
 export const selectedAssets = writable<Set<AssetResponseDto>>(new Set());
 export const selectedGroup = writable<Set<string>>(new Set());
 export const selectedGroup = writable<Set<string>>(new Set());
 export const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0);
 export const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0);
+export const assetSelectionCandidates = writable<Set<AssetResponseDto>>(new Set());
 
 
 function createAssetInteractionStore() {
 function createAssetInteractionStore() {
   let _assetGridState = new AssetGridState();
   let _assetGridState = new AssetGridState();
@@ -19,6 +20,7 @@ function createAssetInteractionStore() {
   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>;
 
 
   // Subscriber
   // Subscriber
   assetGridState.subscribe((state) => {
   assetGridState.subscribe((state) => {
@@ -41,6 +43,10 @@ function createAssetInteractionStore() {
     _assetsInAlbums = assets;
     _assetsInAlbums = assets;
   });
   });
 
 
+  assetSelectionCandidates.subscribe((assets) => {
+    _assetSelectionCandidates = assets;
+  });
+
   // Methods
   // Methods
 
 
   /**
   /**
@@ -117,14 +123,26 @@ function createAssetInteractionStore() {
     selectedGroup.set(_selectedGroup);
     selectedGroup.set(_selectedGroup);
   };
   };
 
 
+  const setAssetSelectionCandidates = (assets: AssetResponseDto[]) => {
+    _assetSelectionCandidates = new Set(assets);
+    assetSelectionCandidates.set(_assetSelectionCandidates);
+  };
+
+  const clearAssetSelectionCandidates = () => {
+    _assetSelectionCandidates.clear();
+    assetSelectionCandidates.set(_assetSelectionCandidates);
+  };
+
   const clearMultiselect = () => {
   const clearMultiselect = () => {
     _selectedAssets.clear();
     _selectedAssets.clear();
     _selectedGroup.clear();
     _selectedGroup.clear();
+    _assetSelectionCandidates.clear();
     _assetsInAlbums = [];
     _assetsInAlbums = [];
 
 
     selectedAssets.set(_selectedAssets);
     selectedAssets.set(_selectedAssets);
     selectedGroup.set(_selectedGroup);
     selectedGroup.set(_selectedGroup);
     assetsInAlbumStoreState.set(_assetsInAlbums);
     assetsInAlbumStoreState.set(_assetsInAlbums);
+    assetSelectionCandidates.set(_assetSelectionCandidates);
   };
   };
 
 
   return {
   return {
@@ -136,6 +154,8 @@ function createAssetInteractionStore() {
     removeAssetFromMultiselectGroup,
     removeAssetFromMultiselectGroup,
     addGroupToMultiselectGroup,
     addGroupToMultiselectGroup,
     removeGroupFromMultiselectGroup,
     removeGroupFromMultiselectGroup,
+    setAssetSelectionCandidates,
+    clearAssetSelectionCandidates,
     clearMultiselect,
     clearMultiselect,
   };
   };
 }
 }

+ 5 - 6
web/src/lib/stores/assets.store.ts

@@ -1,6 +1,5 @@
 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 } from '@api';
-import { flatMap, sumBy } from 'lodash-es';
 import { writable } from 'svelte/store';
 import { writable } from 'svelte/store';
 
 
 /**
 /**
@@ -60,7 +59,7 @@ function createAssetStore() {
 
 
     // Update timeline height based on calculated bucket height
     // Update timeline height based on calculated bucket height
     assetGridState.update((state) => {
     assetGridState.update((state) => {
-      state.timelineHeight = sumBy(state.buckets, (d) => d.bucketHeight);
+      state.timelineHeight = state.buckets.reduce((acc, b) => acc + b.bucketHeight, 0);
       return state;
       return state;
     });
     });
   };
   };
@@ -101,7 +100,7 @@ function createAssetStore() {
         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;
-        state.assets = flatMap(state.buckets, (b) => b.assets);
+        state.assets = state.buckets.flatMap((b) => b.assets);
         return state;
         return state;
       });
       });
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -123,7 +122,7 @@ function createAssetStore() {
       if (state.buckets[bucketIndex].assets.length === 0) {
       if (state.buckets[bucketIndex].assets.length === 0) {
         _removeBucket(state.buckets[bucketIndex].bucketDate);
         _removeBucket(state.buckets[bucketIndex].bucketDate);
       }
       }
-      state.assets = flatMap(state.buckets, (b) => b.assets);
+      state.assets = state.buckets.flatMap((b) => b.assets);
       return state;
       return state;
     });
     });
   };
   };
@@ -132,7 +131,7 @@ function createAssetStore() {
     assetGridState.update((state) => {
     assetGridState.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 = flatMap(state.buckets, (b) => b.assets);
+      state.assets = state.buckets.flatMap((b) => b.assets);
       return state;
       return state;
     });
     });
   };
   };
@@ -180,7 +179,7 @@ function createAssetStore() {
       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;
 
 
-      state.assets = flatMap(state.buckets, (b) => b.assets);
+      state.assets = state.buckets.flatMap((b) => b.assets);
       return state;
       return state;
     });
     });
   };
   };