mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(web): select a range of assets (#3086)
The shift key can be held to select a range of assets. Fixes: #2862
This commit is contained in:
		@@ -3,14 +3,15 @@
 | 
			
		||||
  import { timeToSeconds } from '$lib/utils/time-to-seconds';
 | 
			
		||||
  import { api, AssetResponseDto, AssetTypeEnum, ThumbnailFormat } from '@api';
 | 
			
		||||
  import { createEventDispatcher } from 'svelte';
 | 
			
		||||
  import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.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 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 VideoThumbnail from './video-thumbnail.svelte';
 | 
			
		||||
  import ImageBrokenVariant from 'svelte-material-icons/ImageBrokenVariant.svelte';
 | 
			
		||||
 | 
			
		||||
  const dispatch = createEventDispatcher();
 | 
			
		||||
 | 
			
		||||
@@ -21,6 +22,7 @@
 | 
			
		||||
  export let thumbnailHeight: number | undefined = undefined;
 | 
			
		||||
  export let format: ThumbnailFormat = ThumbnailFormat.Webp;
 | 
			
		||||
  export let selected = false;
 | 
			
		||||
  export let selectionCandidate = false;
 | 
			
		||||
  export let disabled = false;
 | 
			
		||||
  export let readonly = false;
 | 
			
		||||
  export let publicSharedKey: string | undefined = undefined;
 | 
			
		||||
@@ -30,7 +32,7 @@
 | 
			
		||||
 | 
			
		||||
  $: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
 | 
			
		||||
 | 
			
		||||
  $: [width, height] = (() => {
 | 
			
		||||
  $: [width, height] = ((): [number, number] => {
 | 
			
		||||
    if (thumbnailSize) {
 | 
			
		||||
      return [thumbnailSize, thumbnailSize];
 | 
			
		||||
    }
 | 
			
		||||
@@ -42,12 +44,19 @@
 | 
			
		||||
    return [235, 235];
 | 
			
		||||
  })();
 | 
			
		||||
 | 
			
		||||
  const thumbnailClickedHandler = () => {
 | 
			
		||||
  const thumbnailClickedHandler = (e: Event) => {
 | 
			
		||||
    if (!disabled) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      dispatch('click', { asset });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const thumbnailKeyDownHandler = (e: KeyboardEvent) => {
 | 
			
		||||
    if (e.key === 'Enter') {
 | 
			
		||||
      thumbnailClickedHandler(e);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onIconClickedHandler = (e: MouseEvent) => {
 | 
			
		||||
    e.stopPropagation();
 | 
			
		||||
    if (!disabled) {
 | 
			
		||||
@@ -68,21 +77,23 @@
 | 
			
		||||
    on:mouseenter={() => (mouseOver = true)}
 | 
			
		||||
    on:mouseleave={() => (mouseOver = false)}
 | 
			
		||||
    on:click={thumbnailClickedHandler}
 | 
			
		||||
    on:keydown={thumbnailClickedHandler}
 | 
			
		||||
    on:keydown={thumbnailKeyDownHandler}
 | 
			
		||||
  >
 | 
			
		||||
    {#if intersecting}
 | 
			
		||||
      <div class="absolute w-full h-full z-20">
 | 
			
		||||
        <!-- Select asset button  -->
 | 
			
		||||
        {#if !readonly}
 | 
			
		||||
        {#if !readonly && (mouseOver || selected || selectionCandidate)}
 | 
			
		||||
          <button
 | 
			
		||||
            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}
 | 
			
		||||
            role="checkbox"
 | 
			
		||||
            aria-checked={selected}
 | 
			
		||||
            {disabled}
 | 
			
		||||
            in:fade={{ duration: 100 }}
 | 
			
		||||
            out:fade={{ duration: 100 }}
 | 
			
		||||
          >
 | 
			
		||||
            {#if disabled}
 | 
			
		||||
              <CheckCircle size="24" class="text-zinc-800" />
 | 
			
		||||
@@ -153,6 +164,13 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        {/if}
 | 
			
		||||
      </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}
 | 
			
		||||
  </div>
 | 
			
		||||
</IntersectionObserver>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import {
 | 
			
		||||
    assetInteractionStore,
 | 
			
		||||
    assetSelectionCandidates,
 | 
			
		||||
    assetsInAlbumStoreState,
 | 
			
		||||
    isMultiSelectStoreState,
 | 
			
		||||
    selectedAssets,
 | 
			
		||||
@@ -8,15 +9,15 @@
 | 
			
		||||
  } from '$lib/stores/asset-interaction.store';
 | 
			
		||||
  import { assetStore } from '$lib/stores/assets.store';
 | 
			
		||||
  import { locale } from '$lib/stores/preferences.store';
 | 
			
		||||
  import { getAssetRatio } from '$lib/utils/asset-utils';
 | 
			
		||||
  import type { AssetResponseDto } from '@api';
 | 
			
		||||
  import justifiedLayout from 'justified-layout';
 | 
			
		||||
  import lodash from 'lodash-es';
 | 
			
		||||
  import { createEventDispatcher } from 'svelte';
 | 
			
		||||
  import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
 | 
			
		||||
  import CircleOutline from 'svelte-material-icons/CircleOutline.svelte';
 | 
			
		||||
  import { fly } from 'svelte/transition';
 | 
			
		||||
  import { getAssetRatio } from '$lib/utils/asset-utils';
 | 
			
		||||
  import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
 | 
			
		||||
  import { createEventDispatcher } from 'svelte';
 | 
			
		||||
 | 
			
		||||
  export let assets: AssetResponseDto[];
 | 
			
		||||
  export let bucketDate: string;
 | 
			
		||||
@@ -130,18 +131,19 @@
 | 
			
		||||
    dateGroupTitle: string,
 | 
			
		||||
  ) => {
 | 
			
		||||
    if ($selectedAssets.has(asset)) {
 | 
			
		||||
      for (const candidate of $assetSelectionCandidates || []) {
 | 
			
		||||
        assetInteractionStore.removeAssetFromMultiselectGroup(candidate);
 | 
			
		||||
      }
 | 
			
		||||
      assetInteractionStore.removeAssetFromMultiselectGroup(asset);
 | 
			
		||||
    } else {
 | 
			
		||||
      for (const candidate of $assetSelectionCandidates || []) {
 | 
			
		||||
        assetInteractionStore.addAssetToMultiselectGroup(candidate);
 | 
			
		||||
      }
 | 
			
		||||
      assetInteractionStore.addAssetToMultiselectGroup(asset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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 (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
 | 
			
		||||
    hoveredDateGroup = dateGroupTitle;
 | 
			
		||||
 | 
			
		||||
    if ($isMultiSelectStoreState) {
 | 
			
		||||
      dispatch('selectAssetCandidates', { asset });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@@ -171,9 +177,12 @@
 | 
			
		||||
      class="flex flex-col mt-5"
 | 
			
		||||
      on:mouseenter={() => {
 | 
			
		||||
        isMouseOverGroup = true;
 | 
			
		||||
        assetMouseEventHandler(dateGroupTitle);
 | 
			
		||||
        assetMouseEventHandler(dateGroupTitle, null);
 | 
			
		||||
      }}
 | 
			
		||||
      on:mouseleave={() => {
 | 
			
		||||
        isMouseOverGroup = false;
 | 
			
		||||
        assetMouseEventHandler(dateGroupTitle, null);
 | 
			
		||||
      }}
 | 
			
		||||
      on:mouseleave={() => (isMouseOverGroup = false)}
 | 
			
		||||
    >
 | 
			
		||||
      <!-- Date group title -->
 | 
			
		||||
      <p
 | 
			
		||||
@@ -216,9 +225,10 @@
 | 
			
		||||
              {groupIndex}
 | 
			
		||||
              on:click={() => assetClickHandler(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}
 | 
			
		||||
              thumbnailHeight={box.height}
 | 
			
		||||
            />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,15 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { BucketPosition } from '$lib/models/asset-grid-state';
 | 
			
		||||
  import {
 | 
			
		||||
    assetInteractionStore,
 | 
			
		||||
    isMultiSelectStoreState,
 | 
			
		||||
    isViewingAssetStoreState,
 | 
			
		||||
    selectedAssets,
 | 
			
		||||
    viewingAssetStoreState,
 | 
			
		||||
  } from '$lib/stores/asset-interaction.store';
 | 
			
		||||
  import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
 | 
			
		||||
  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 AssetViewer from '../asset-viewer/asset-viewer.svelte';
 | 
			
		||||
  import IntersectionObserver from '../asset-viewer/intersection-observer.svelte';
 | 
			
		||||
@@ -16,7 +19,6 @@
 | 
			
		||||
    OnScrollbarDragDetail,
 | 
			
		||||
  } from '../shared-components/scrollbar/scrollbar.svelte';
 | 
			
		||||
  import AssetDateGroup from './asset-date-group.svelte';
 | 
			
		||||
  import { BucketPosition } from '$lib/models/asset-grid-state';
 | 
			
		||||
  import MemoryLane from './memory-lane.svelte';
 | 
			
		||||
 | 
			
		||||
  export let user: UserResponseDto | undefined = undefined;
 | 
			
		||||
@@ -111,8 +113,80 @@
 | 
			
		||||
    navigateToNextAsset();
 | 
			
		||||
    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>
 | 
			
		||||
 | 
			
		||||
<svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} on:selectstart={onSelectStart} />
 | 
			
		||||
 | 
			
		||||
{#if bucketInfo && viewportHeight && $assetGridState.timelineHeight > viewportHeight}
 | 
			
		||||
  <Scrollbar
 | 
			
		||||
    scrollbarHeight={viewportHeight}
 | 
			
		||||
@@ -155,6 +229,7 @@
 | 
			
		||||
              <AssetDateGroup
 | 
			
		||||
                {isAlbumSelectionMode}
 | 
			
		||||
                on:shift={handleScrollTimeline}
 | 
			
		||||
                on:selectAssetCandidates={handleSelectAssetCandidates}
 | 
			
		||||
                assets={bucket.assets}
 | 
			
		||||
                bucketDate={bucket.bucketDate}
 | 
			
		||||
                bucketHeight={bucket.bucketHeight}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ export const assetsInAlbumStoreState = writable<AssetResponseDto[]>([]);
 | 
			
		||||
export const selectedAssets = writable<Set<AssetResponseDto>>(new Set());
 | 
			
		||||
export const selectedGroup = writable<Set<string>>(new Set());
 | 
			
		||||
export const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0);
 | 
			
		||||
export const assetSelectionCandidates = writable<Set<AssetResponseDto>>(new Set());
 | 
			
		||||
 | 
			
		||||
function createAssetInteractionStore() {
 | 
			
		||||
  let _assetGridState = new AssetGridState();
 | 
			
		||||
@@ -19,6 +20,7 @@ function createAssetInteractionStore() {
 | 
			
		||||
  let _selectedAssets: Set<AssetResponseDto>;
 | 
			
		||||
  let _selectedGroup: Set<string>;
 | 
			
		||||
  let _assetsInAlbums: AssetResponseDto[];
 | 
			
		||||
  let _assetSelectionCandidates: Set<AssetResponseDto>;
 | 
			
		||||
 | 
			
		||||
  // Subscriber
 | 
			
		||||
  assetGridState.subscribe((state) => {
 | 
			
		||||
@@ -41,6 +43,10 @@ function createAssetInteractionStore() {
 | 
			
		||||
    _assetsInAlbums = assets;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  assetSelectionCandidates.subscribe((assets) => {
 | 
			
		||||
    _assetSelectionCandidates = assets;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Methods
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -117,14 +123,26 @@ function createAssetInteractionStore() {
 | 
			
		||||
    selectedGroup.set(_selectedGroup);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const setAssetSelectionCandidates = (assets: AssetResponseDto[]) => {
 | 
			
		||||
    _assetSelectionCandidates = new Set(assets);
 | 
			
		||||
    assetSelectionCandidates.set(_assetSelectionCandidates);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const clearAssetSelectionCandidates = () => {
 | 
			
		||||
    _assetSelectionCandidates.clear();
 | 
			
		||||
    assetSelectionCandidates.set(_assetSelectionCandidates);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const clearMultiselect = () => {
 | 
			
		||||
    _selectedAssets.clear();
 | 
			
		||||
    _selectedGroup.clear();
 | 
			
		||||
    _assetSelectionCandidates.clear();
 | 
			
		||||
    _assetsInAlbums = [];
 | 
			
		||||
 | 
			
		||||
    selectedAssets.set(_selectedAssets);
 | 
			
		||||
    selectedGroup.set(_selectedGroup);
 | 
			
		||||
    assetsInAlbumStoreState.set(_assetsInAlbums);
 | 
			
		||||
    assetSelectionCandidates.set(_assetSelectionCandidates);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
@@ -136,6 +154,8 @@ function createAssetInteractionStore() {
 | 
			
		||||
    removeAssetFromMultiselectGroup,
 | 
			
		||||
    addGroupToMultiselectGroup,
 | 
			
		||||
    removeGroupFromMultiselectGroup,
 | 
			
		||||
    setAssetSelectionCandidates,
 | 
			
		||||
    clearAssetSelectionCandidates,
 | 
			
		||||
    clearMultiselect,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import { AssetGridState, BucketPosition } from '$lib/models/asset-grid-state';
 | 
			
		||||
import { api, AssetCountByTimeBucketResponseDto } from '@api';
 | 
			
		||||
import { flatMap, sumBy } from 'lodash-es';
 | 
			
		||||
import { writable } from 'svelte/store';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -60,7 +59,7 @@ function createAssetStore() {
 | 
			
		||||
 | 
			
		||||
    // Update timeline height based on calculated bucket height
 | 
			
		||||
    assetGridState.update((state) => {
 | 
			
		||||
      state.timelineHeight = sumBy(state.buckets, (d) => d.bucketHeight);
 | 
			
		||||
      state.timelineHeight = state.buckets.reduce((acc, b) => acc + b.bucketHeight, 0);
 | 
			
		||||
      return state;
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
@@ -101,7 +100,7 @@ function createAssetStore() {
 | 
			
		||||
        const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucket);
 | 
			
		||||
        state.buckets[bucketIndex].assets = assets;
 | 
			
		||||
        state.buckets[bucketIndex].position = position;
 | 
			
		||||
        state.assets = flatMap(state.buckets, (b) => b.assets);
 | 
			
		||||
        state.assets = state.buckets.flatMap((b) => b.assets);
 | 
			
		||||
        return state;
 | 
			
		||||
      });
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
@@ -123,7 +122,7 @@ function createAssetStore() {
 | 
			
		||||
      if (state.buckets[bucketIndex].assets.length === 0) {
 | 
			
		||||
        _removeBucket(state.buckets[bucketIndex].bucketDate);
 | 
			
		||||
      }
 | 
			
		||||
      state.assets = flatMap(state.buckets, (b) => b.assets);
 | 
			
		||||
      state.assets = state.buckets.flatMap((b) => b.assets);
 | 
			
		||||
      return state;
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
@@ -132,7 +131,7 @@ function createAssetStore() {
 | 
			
		||||
    assetGridState.update((state) => {
 | 
			
		||||
      const bucketIndex = state.buckets.findIndex((b) => b.bucketDate === bucketDate);
 | 
			
		||||
      state.buckets.splice(bucketIndex, 1);
 | 
			
		||||
      state.assets = flatMap(state.buckets, (b) => b.assets);
 | 
			
		||||
      state.assets = state.buckets.flatMap((b) => b.assets);
 | 
			
		||||
      return state;
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
@@ -180,7 +179,7 @@ function createAssetStore() {
 | 
			
		||||
      const assetIndex = state.buckets[bucketIndex].assets.findIndex((a) => a.id === assetId);
 | 
			
		||||
      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;
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user