mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:37:11 -04:00 
			
		
		
		
	refactor(web): asset interaction (#14662)
* refactor(web): asset interaction * feedback
This commit is contained in:
		
							parent
							
								
									525840b040
								
							
						
					
					
						commit
						b5022d80d6
					
				| @ -4,7 +4,6 @@ | |||||||
|   import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; |   import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; | ||||||
|   import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; |   import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; | ||||||
|   import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk'; |   import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk'; | ||||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import { AssetStore } from '$lib/stores/assets.store'; |   import { AssetStore } from '$lib/stores/assets.store'; | ||||||
|   import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils'; |   import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils'; | ||||||
|   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; |   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | ||||||
| @ -20,6 +19,7 @@ | |||||||
|   import AlbumSummary from './album-summary.svelte'; |   import AlbumSummary from './album-summary.svelte'; | ||||||
|   import { t } from 'svelte-i18n'; |   import { t } from 'svelte-i18n'; | ||||||
|   import { onDestroy } from 'svelte'; |   import { onDestroy } from 'svelte'; | ||||||
|  |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     sharedLink: SharedLinkResponseDto; |     sharedLink: SharedLinkResponseDto; | ||||||
| @ -34,8 +34,7 @@ | |||||||
|   let { isViewing: showAssetViewer } = assetViewingStore; |   let { isViewing: showAssetViewer } = assetViewingStore; | ||||||
| 
 | 
 | ||||||
|   const assetStore = new AssetStore({ albumId: album.id, order: album.order }); |   const assetStore = new AssetStore({ albumId: album.id, order: album.order }); | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
|   const { isMultiSelectState, selectedAssets } = assetInteractionStore; |  | ||||||
| 
 | 
 | ||||||
|   dragAndDropFilesStore.subscribe((value) => { |   dragAndDropFilesStore.subscribe((value) => { | ||||||
|     if (value.isDragging && value.files.length > 0) { |     if (value.isDragging && value.files.length > 0) { | ||||||
| @ -52,8 +51,8 @@ | |||||||
|   use:shortcut={{ |   use:shortcut={{ | ||||||
|     shortcut: { key: 'Escape' }, |     shortcut: { key: 'Escape' }, | ||||||
|     onShortcut: () => { |     onShortcut: () => { | ||||||
|       if (!$showAssetViewer && $isMultiSelectState) { |       if (!$showAssetViewer && assetInteraction.selectionActive) { | ||||||
|         cancelMultiselect(assetInteractionStore); |         cancelMultiselect(assetInteraction); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|   }} |   }} | ||||||
| @ -61,13 +60,13 @@ | |||||||
| /> | /> | ||||||
| 
 | 
 | ||||||
| <header> | <header> | ||||||
|   {#if $isMultiSelectState} |   {#if assetInteraction.selectionActive} | ||||||
|     <AssetSelectControlBar |     <AssetSelectControlBar | ||||||
|       ownerId={user?.id} |       ownerId={user?.id} | ||||||
|       assets={$selectedAssets} |       assets={assetInteraction.selectedAssets} | ||||||
|       clearSelect={() => assetInteractionStore.clearMultiselect()} |       clearSelect={() => assetInteraction.clearMultiselect()} | ||||||
|     > |     > | ||||||
|       <SelectAllAssets {assetStore} {assetInteractionStore} /> |       <SelectAllAssets {assetStore} {assetInteraction} /> | ||||||
|       {#if sharedLink.allowDownload} |       {#if sharedLink.allowDownload} | ||||||
|         <DownloadAction filename="{album.albumName}.zip" /> |         <DownloadAction filename="{album.albumName}.zip" /> | ||||||
|       {/if} |       {/if} | ||||||
| @ -102,7 +101,7 @@ | |||||||
| </header> | </header> | ||||||
| 
 | 
 | ||||||
| <main class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg"> | <main class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg"> | ||||||
|   <AssetGrid enableRouting={true} {album} {assetStore} {assetInteractionStore}> |   <AssetGrid enableRouting={true} {album} {assetStore} {assetInteraction}> | ||||||
|     <section class="pt-8 md:pt-24"> |     <section class="pt-8 md:pt-24"> | ||||||
|       <!-- ALBUM TITLE --> |       <!-- ALBUM TITLE --> | ||||||
|       <h1 |       <h1 | ||||||
|  | |||||||
| @ -18,7 +18,6 @@ | |||||||
|   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; |   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; | ||||||
|   import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; |   import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; | ||||||
|   import { cancelMultiselect } from '$lib/utils/asset-utils'; |   import { cancelMultiselect } from '$lib/utils/asset-utils'; | ||||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import { AppRoute, QueryParameter } from '$lib/constants'; |   import { AppRoute, QueryParameter } from '$lib/constants'; | ||||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; |   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||||
|   import { type Viewport } from '$lib/stores/assets.store'; |   import { type Viewport } from '$lib/stores/assets.store'; | ||||||
| @ -46,8 +45,9 @@ | |||||||
|   import { tweened } from 'svelte/motion'; |   import { tweened } from 'svelte/motion'; | ||||||
|   import { derived as storeDerived } from 'svelte/store'; |   import { derived as storeDerived } from 'svelte/store'; | ||||||
|   import { fade } from 'svelte/transition'; |   import { fade } from 'svelte/transition'; | ||||||
|   import { preferences, user } from '$lib/stores/user.store'; |   import { preferences } from '$lib/stores/user.store'; | ||||||
|   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; |   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; | ||||||
|  |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   type MemoryIndex = { |   type MemoryIndex = { | ||||||
|     memoryIndex: number; |     memoryIndex: number; | ||||||
| @ -73,8 +73,7 @@ | |||||||
| 
 | 
 | ||||||
|   const { isViewing } = assetViewingStore; |   const { isViewing } = assetViewingStore; | ||||||
|   const viewport: Viewport = $state({ width: 0, height: 0 }); |   const viewport: Viewport = $state({ width: 0, height: 0 }); | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
|   const { selectedAssets } = assetInteractionStore; |  | ||||||
|   const progressBarController = tweened<number>(0, { |   const progressBarController = tweened<number>(0, { | ||||||
|     duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0), |     duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0), | ||||||
|   }); |   }); | ||||||
| @ -130,7 +129,7 @@ | |||||||
|   const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]); |   const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]); | ||||||
|   const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]); |   const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]); | ||||||
|   const handleEscape = async () => goto(AppRoute.PHOTOS); |   const handleEscape = async () => goto(AppRoute.PHOTOS); | ||||||
|   const handleSelectAll = () => assetInteractionStore.selectAssets(current?.memory.assets || []); |   const handleSelectAll = () => assetInteraction.selectAssets(current?.memory.assets || []); | ||||||
|   const handleAction = async (action: 'reset' | 'pause' | 'play') => { |   const handleAction = async (action: 'reset' | 'pause' | 'play') => { | ||||||
|     switch (action) { |     switch (action) { | ||||||
|       case 'play': { |       case 'play': { | ||||||
| @ -212,10 +211,6 @@ | |||||||
|     current = loadFromParams($memories, target); |     current = loadFromParams($memories, target); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   let isMultiSelectionMode = $derived($selectedAssets.size > 0); |  | ||||||
|   let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); |  | ||||||
|   let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); |  | ||||||
| 
 |  | ||||||
|   $effect(() => { |   $effect(() => { | ||||||
|     handlePromiseError(handleProgress($progressBarController)); |     handlePromiseError(handleProgress($progressBarController)); | ||||||
|   }); |   }); | ||||||
| @ -223,7 +218,6 @@ | |||||||
|   $effect(() => { |   $effect(() => { | ||||||
|     handlePromiseError(handleAction(galleryInView ? 'pause' : 'play')); |     handlePromiseError(handleAction(galleryInView ? 'pause' : 'play')); | ||||||
|   }); |   }); | ||||||
|   let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <svelte:window | <svelte:window | ||||||
| @ -238,9 +232,12 @@ | |||||||
|       ]} |       ]} | ||||||
| /> | /> | ||||||
| 
 | 
 | ||||||
| {#if isMultiSelectionMode} | {#if assetInteraction.selectionActive} | ||||||
|   <div class="sticky top-0 z-[90]"> |   <div class="sticky top-0 z-[90]"> | ||||||
|     <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => cancelMultiselect(assetInteractionStore)}> |     <AssetSelectControlBar | ||||||
|  |       assets={assetInteraction.selectedAssets} | ||||||
|  |       clearSelect={() => cancelMultiselect(assetInteraction)} | ||||||
|  |     > | ||||||
|       <CreateSharedLink /> |       <CreateSharedLink /> | ||||||
|       <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} /> |       <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} /> | ||||||
| 
 | 
 | ||||||
| @ -249,14 +246,14 @@ | |||||||
|         <AddToAlbum shared /> |         <AddToAlbum shared /> | ||||||
|       </ButtonContextMenu> |       </ButtonContextMenu> | ||||||
| 
 | 
 | ||||||
|       <FavoriteAction removeFavorite={isAllFavorite} onFavorite={handleUpdate} /> |       <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={handleUpdate} /> | ||||||
| 
 | 
 | ||||||
|       <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}> |       <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}> | ||||||
|         <DownloadAction menuItem /> |         <DownloadAction menuItem /> | ||||||
|         <ChangeDate menuItem /> |         <ChangeDate menuItem /> | ||||||
|         <ChangeLocation menuItem /> |         <ChangeLocation menuItem /> | ||||||
|         <ArchiveAction menuItem unarchive={isAllArchived} onArchive={handleRemove} /> |         <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={handleRemove} /> | ||||||
|         {#if $preferences.tags.enabled && isAllUserOwned} |         {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} | ||||||
|           <TagAction menuItem /> |           <TagAction menuItem /> | ||||||
|         {/if} |         {/if} | ||||||
|         <DeleteAssets menuItem onAssetDelete={handleRemove} /> |         <DeleteAssets menuItem onAssetDelete={handleRemove} /> | ||||||
| @ -490,7 +487,7 @@ | |||||||
|           onPrevious={handlePreviousAsset} |           onPrevious={handlePreviousAsset} | ||||||
|           assets={current.memory.assets} |           assets={current.memory.assets} | ||||||
|           {viewport} |           {viewport} | ||||||
|           {assetInteractionStore} |           {assetInteraction} | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|     </section> |     </section> | ||||||
|  | |||||||
| @ -1,24 +1,24 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; |   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||||
|   import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets.store'; |   import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets.store'; | ||||||
|   import { mdiSelectAll, mdiSelectRemove } from '@mdi/js'; |   import { mdiSelectAll, mdiSelectRemove } from '@mdi/js'; | ||||||
|   import { selectAllAssets, cancelMultiselect } from '$lib/utils/asset-utils'; |   import { selectAllAssets, cancelMultiselect } from '$lib/utils/asset-utils'; | ||||||
|   import { t } from 'svelte-i18n'; |   import { t } from 'svelte-i18n'; | ||||||
|  |   import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     assetStore: AssetStore; |     assetStore: AssetStore; | ||||||
|     assetInteractionStore: AssetInteractionStore; |     assetInteraction: AssetInteraction; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   let { assetStore, assetInteractionStore }: Props = $props(); |   let { assetStore, assetInteraction }: Props = $props(); | ||||||
| 
 | 
 | ||||||
|   const handleSelectAll = async () => { |   const handleSelectAll = async () => { | ||||||
|     await selectAllAssets(assetStore, assetInteractionStore); |     await selectAllAssets(assetStore, assetInteraction); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleCancel = () => { |   const handleCancel = () => { | ||||||
|     cancelMultiselect(assetInteractionStore); |     cancelMultiselect(assetInteraction); | ||||||
|   }; |   }; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ | |||||||
|   import { intersectionObserver } from '$lib/actions/intersection-observer'; |   import { intersectionObserver } from '$lib/actions/intersection-observer'; | ||||||
|   import Icon from '$lib/components/elements/icon.svelte'; |   import Icon from '$lib/components/elements/icon.svelte'; | ||||||
|   import Skeleton from '$lib/components/photos-page/skeleton.svelte'; |   import Skeleton from '$lib/components/photos-page/skeleton.svelte'; | ||||||
|   import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import { AssetBucket, type AssetStore, type Viewport } from '$lib/stores/assets.store'; |   import { AssetBucket, type AssetStore, type Viewport } from '$lib/stores/assets.store'; | ||||||
|   import { navigate } from '$lib/utils/navigation'; |   import { navigate } from '$lib/utils/navigation'; | ||||||
|   import { findTotalOffset, type DateGroup, type ScrollTargetListener } from '$lib/utils/timeline-util'; |   import { findTotalOffset, type DateGroup, type ScrollTargetListener } from '$lib/utils/timeline-util'; | ||||||
| @ -13,6 +12,7 @@ | |||||||
|   import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; |   import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; | ||||||
|   import { TUNABLES } from '$lib/utils/tunables'; |   import { TUNABLES } from '$lib/utils/tunables'; | ||||||
|   import { generateId } from '$lib/utils/generate-id'; |   import { generateId } from '$lib/utils/generate-id'; | ||||||
|  |   import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   export let element: HTMLElement | undefined = undefined; |   export let element: HTMLElement | undefined = undefined; | ||||||
|   export let isSelectionMode = false; |   export let isSelectionMode = false; | ||||||
| @ -25,7 +25,7 @@ | |||||||
|   export let renderThumbsAtTopMargin: string | undefined = undefined; |   export let renderThumbsAtTopMargin: string | undefined = undefined; | ||||||
|   export let assetStore: AssetStore; |   export let assetStore: AssetStore; | ||||||
|   export let bucket: AssetBucket; |   export let bucket: AssetBucket; | ||||||
|   export let assetInteractionStore: AssetInteractionStore; |   export let assetInteraction: AssetInteraction; | ||||||
| 
 | 
 | ||||||
|   export let onScrollTarget: ScrollTargetListener | undefined = undefined; |   export let onScrollTarget: ScrollTargetListener | undefined = undefined; | ||||||
|   export let onAssetInGrid: ((asset: AssetResponseDto) => void) | undefined = undefined; |   export let onAssetInGrid: ((asset: AssetResponseDto) => void) | undefined = undefined; | ||||||
| @ -43,13 +43,11 @@ | |||||||
|   /* TODO figure out a way to calculate this*/ |   /* TODO figure out a way to calculate this*/ | ||||||
|   const TITLE_HEIGHT = 51; |   const TITLE_HEIGHT = 51; | ||||||
| 
 | 
 | ||||||
|   const { selectedGroup, selectedAssets, assetSelectionCandidates, isMultiSelectState } = assetInteractionStore; |  | ||||||
| 
 |  | ||||||
|   let isMouseOverGroup = false; |   let isMouseOverGroup = false; | ||||||
|   let hoveredDateGroup = ''; |   let hoveredDateGroup = ''; | ||||||
| 
 | 
 | ||||||
|   const onClick = (assets: AssetResponseDto[], groupTitle: string, asset: AssetResponseDto) => { |   const onClick = (assets: AssetResponseDto[], groupTitle: string, asset: AssetResponseDto) => { | ||||||
|     if (isSelectionMode || $isMultiSelectState) { |     if (isSelectionMode || assetInteraction.selectionActive) { | ||||||
|       assetSelectHandler(asset, assets, groupTitle); |       assetSelectHandler(asset, assets, groupTitle); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @ -69,13 +67,15 @@ | |||||||
|     onSelectAssets(asset); |     onSelectAssets(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 = assetsInDateGroup.filter((asset) => $selectedAssets.has(asset)).length; |     let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => | ||||||
|  |       assetInteraction.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) { | ||||||
|       assetInteractionStore.addGroupToMultiselectGroup(groupTitle); |       assetInteraction.addGroupToMultiselectGroup(groupTitle); | ||||||
|     } else { |     } else { | ||||||
|       assetInteractionStore.removeGroupFromMultiselectGroup(groupTitle); |       assetInteraction.removeGroupFromMultiselectGroup(groupTitle); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
| @ -83,7 +83,7 @@ | |||||||
|     // Show multi select icon on hover on date group |     // Show multi select icon on hover on date group | ||||||
|     hoveredDateGroup = groupTitle; |     hoveredDateGroup = groupTitle; | ||||||
| 
 | 
 | ||||||
|     if ($isMultiSelectState) { |     if (assetInteraction.selectionActive) { | ||||||
|       onSelectAssetCandidates(asset); |       onSelectAssetCandidates(asset); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| @ -151,14 +151,14 @@ | |||||||
|             class="flex z-[100] sticky top-[-1px] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm" |             class="flex z-[100] sticky top-[-1px] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm" | ||||||
|             style:width={dateGroup.geometry.containerWidth + 'px'} |             style:width={dateGroup.geometry.containerWidth + 'px'} | ||||||
|           > |           > | ||||||
|             {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroup.groupTitle))} |             {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))} | ||||||
|               <div |               <div | ||||||
|                 transition:fly={{ x: -24, duration: 200, opacity: 0.5 }} |                 transition:fly={{ x: -24, duration: 200, opacity: 0.5 }} | ||||||
|                 class="inline-block px-2 hover:cursor-pointer" |                 class="inline-block px-2 hover:cursor-pointer" | ||||||
|                 on:click={() => handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)} |                 on:click={() => handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)} | ||||||
|                 on:keydown={() => handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)} |                 on:keydown={() => handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)} | ||||||
|               > |               > | ||||||
|                 {#if $selectedGroup.has(dateGroup.groupTitle)} |                 {#if assetInteraction.selectedGroup.has(dateGroup.groupTitle)} | ||||||
|                   <Icon path={mdiCheckCircle} size="24" color="#4250af" /> |                   <Icon path={mdiCheckCircle} size="24" color="#4250af" /> | ||||||
|                 {:else} |                 {:else} | ||||||
|                   <Icon path={mdiCircleOutline} size="24" color="#757575" /> |                   <Icon path={mdiCircleOutline} size="24" color="#757575" /> | ||||||
| @ -212,8 +212,8 @@ | |||||||
|                   onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)} |                   onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)} | ||||||
|                   onSelect={(asset) => assetSelectHandler(asset, dateGroup.assets, dateGroup.groupTitle)} |                   onSelect={(asset) => assetSelectHandler(asset, dateGroup.assets, dateGroup.groupTitle)} | ||||||
|                   onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, asset)} |                   onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, asset)} | ||||||
|                   selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} |                   selected={assetInteraction.selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} | ||||||
|                   selectionCandidate={$assetSelectionCandidates.has(asset)} |                   selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)} | ||||||
|                   disabled={$assetStore.albumAssets.has(asset.id)} |                   disabled={$assetStore.albumAssets.has(asset.id)} | ||||||
|                   thumbnailWidth={box.width} |                   thumbnailWidth={box.width} | ||||||
|                   thumbnailHeight={box.height} |                   thumbnailHeight={box.height} | ||||||
|  | |||||||
| @ -3,7 +3,6 @@ | |||||||
|   import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut'; |   import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut'; | ||||||
|   import type { Action } from '$lib/components/asset-viewer/actions/action'; |   import type { Action } from '$lib/components/asset-viewer/actions/action'; | ||||||
|   import { AppRoute, AssetAction } from '$lib/constants'; |   import { AppRoute, AssetAction } from '$lib/constants'; | ||||||
|   import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; |   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||||
|   import { AssetBucket, AssetStore, type BucketListener, type ViewportXY } from '$lib/stores/assets.store'; |   import { AssetBucket, AssetStore, type BucketListener, type ViewportXY } from '$lib/stores/assets.store'; | ||||||
|   import { locale, showDeleteModal } from '$lib/stores/preferences.store'; |   import { locale, showDeleteModal } from '$lib/stores/preferences.store'; | ||||||
| @ -37,6 +36,7 @@ | |||||||
|   import type { UpdatePayload } from 'vite'; |   import type { UpdatePayload } from 'vite'; | ||||||
|   import { generateId } from '$lib/utils/generate-id'; |   import { generateId } from '$lib/utils/generate-id'; | ||||||
|   import { isTimelineScrolling } from '$lib/stores/timeline.store'; |   import { isTimelineScrolling } from '$lib/stores/timeline.store'; | ||||||
|  |   import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     isSelectionMode?: boolean; |     isSelectionMode?: boolean; | ||||||
| @ -46,7 +46,7 @@ | |||||||
|    additionally, update the page location/url with the asset as the asset-grid is scrolled */ |    additionally, update the page location/url with the asset as the asset-grid is scrolled */ | ||||||
|     enableRouting: boolean; |     enableRouting: boolean; | ||||||
|     assetStore: AssetStore; |     assetStore: AssetStore; | ||||||
|     assetInteractionStore: AssetInteractionStore; |     assetInteraction: AssetInteraction; | ||||||
|     removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.FAVORITE | AssetAction.UNFAVORITE | null; |     removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.FAVORITE | AssetAction.UNFAVORITE | null; | ||||||
|     withStacked?: boolean; |     withStacked?: boolean; | ||||||
|     showArchiveIcon?: boolean; |     showArchiveIcon?: boolean; | ||||||
| @ -64,7 +64,7 @@ | |||||||
|     singleSelect = false, |     singleSelect = false, | ||||||
|     enableRouting, |     enableRouting, | ||||||
|     assetStore = $bindable(), |     assetStore = $bindable(), | ||||||
|     assetInteractionStore, |     assetInteraction, | ||||||
|     removeAction = null, |     removeAction = null, | ||||||
|     withStacked = false, |     withStacked = false, | ||||||
|     showArchiveIcon = false, |     showArchiveIcon = false, | ||||||
| @ -78,8 +78,6 @@ | |||||||
|   }: Props = $props(); |   }: Props = $props(); | ||||||
| 
 | 
 | ||||||
|   let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore; |   let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore; | ||||||
|   const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } = |  | ||||||
|     assetInteractionStore; |  | ||||||
| 
 | 
 | ||||||
|   const viewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 }); |   const viewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 }); | ||||||
|   const safeViewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 }); |   const safeViewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 }); | ||||||
| @ -437,11 +435,11 @@ | |||||||
|       (assetIds) => $assetStore.removeAssets(assetIds), |       (assetIds) => $assetStore.removeAssets(assetIds), | ||||||
|       idsSelectedAssets, |       idsSelectedAssets, | ||||||
|     ); |     ); | ||||||
|     assetInteractionStore.clearMultiselect(); |     assetInteraction.clearMultiselect(); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const onDelete = () => { |   const onDelete = () => { | ||||||
|     const hasTrashedAsset = Array.from($selectedAssets).some((asset) => asset.isTrashed); |     const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed); | ||||||
| 
 | 
 | ||||||
|     if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) { |     if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) { | ||||||
|       isShowDeleteConfirmation = true; |       isShowDeleteConfirmation = true; | ||||||
| @ -459,7 +457,7 @@ | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const onStackAssets = async () => { |   const onStackAssets = async () => { | ||||||
|     const ids = await stackAssets(Array.from($selectedAssets)); |     const ids = await stackAssets(assetInteraction.selectedAssetsArray); | ||||||
|     if (ids) { |     if (ids) { | ||||||
|       $assetStore.removeAssets(ids); |       $assetStore.removeAssets(ids); | ||||||
|       onEscape(); |       onEscape(); | ||||||
| @ -467,7 +465,7 @@ | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const toggleArchive = async () => { |   const toggleArchive = async () => { | ||||||
|     const ids = await archiveAssets(Array.from($selectedAssets), !isAllArchived); |     const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived); | ||||||
|     if (ids) { |     if (ids) { | ||||||
|       $assetStore.removeAssets(ids); |       $assetStore.removeAssets(ids); | ||||||
|       deselectAllAssets(); |       deselectAllAssets(); | ||||||
| @ -482,7 +480,7 @@ | |||||||
| 
 | 
 | ||||||
|   const handleSelectAsset = (asset: AssetResponseDto) => { |   const handleSelectAsset = (asset: AssetResponseDto) => { | ||||||
|     if (!$assetStore.albumAssets.has(asset.id)) { |     if (!$assetStore.albumAssets.has(asset.id)) { | ||||||
|       assetInteractionStore.selectAsset(asset); |       assetInteraction.selectAsset(asset); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
| @ -573,7 +571,7 @@ | |||||||
|   let shiftKeyIsDown = $state(false); |   let shiftKeyIsDown = $state(false); | ||||||
| 
 | 
 | ||||||
|   const deselectAllAssets = () => { |   const deselectAllAssets = () => { | ||||||
|     cancelMultiselect(assetInteractionStore); |     cancelMultiselect(assetInteraction); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const onKeyDown = (event: KeyboardEvent) => { |   const onKeyDown = (event: KeyboardEvent) => { | ||||||
| @ -606,13 +604,13 @@ | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleGroupSelect = (group: string, assets: AssetResponseDto[]) => { |   const handleGroupSelect = (group: string, assets: AssetResponseDto[]) => { | ||||||
|     if ($selectedGroup.has(group)) { |     if (assetInteraction.selectedGroup.has(group)) { | ||||||
|       assetInteractionStore.removeGroupFromMultiselectGroup(group); |       assetInteraction.removeGroupFromMultiselectGroup(group); | ||||||
|       for (const asset of assets) { |       for (const asset of assets) { | ||||||
|         assetInteractionStore.removeAssetFromMultiselectGroup(asset); |         assetInteraction.removeAssetFromMultiselectGroup(asset); | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       assetInteractionStore.addGroupToMultiselectGroup(group); |       assetInteraction.addGroupToMultiselectGroup(group); | ||||||
|       for (const asset of assets) { |       for (const asset of assets) { | ||||||
|         handleSelectAsset(asset); |         handleSelectAsset(asset); | ||||||
|       } |       } | ||||||
| @ -631,26 +629,26 @@ | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const rangeSelection = $assetSelectionCandidates.size > 0; |     const rangeSelection = assetInteraction.assetSelectionCandidates.size > 0; | ||||||
|     const deselect = $selectedAssets.has(asset); |     const deselect = assetInteraction.selectedAssets.has(asset); | ||||||
| 
 | 
 | ||||||
|     // Select/deselect already loaded assets |     // Select/deselect already loaded assets | ||||||
|     if (deselect) { |     if (deselect) { | ||||||
|       for (const candidate of $assetSelectionCandidates || []) { |       for (const candidate of assetInteraction.assetSelectionCandidates) { | ||||||
|         assetInteractionStore.removeAssetFromMultiselectGroup(candidate); |         assetInteraction.removeAssetFromMultiselectGroup(candidate); | ||||||
|       } |       } | ||||||
|       assetInteractionStore.removeAssetFromMultiselectGroup(asset); |       assetInteraction.removeAssetFromMultiselectGroup(asset); | ||||||
|     } else { |     } else { | ||||||
|       for (const candidate of $assetSelectionCandidates || []) { |       for (const candidate of assetInteraction.assetSelectionCandidates) { | ||||||
|         handleSelectAsset(candidate); |         handleSelectAsset(candidate); | ||||||
|       } |       } | ||||||
|       handleSelectAsset(asset); |       handleSelectAsset(asset); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     assetInteractionStore.clearAssetSelectionCandidates(); |     assetInteraction.clearAssetSelectionCandidates(); | ||||||
| 
 | 
 | ||||||
|     if ($assetSelectionStart && rangeSelection) { |     if (assetInteraction.assetSelectionStart && rangeSelection) { | ||||||
|       let startBucketIndex = $assetStore.getBucketIndexByAssetId($assetSelectionStart.id); |       let startBucketIndex = $assetStore.getBucketIndexByAssetId(assetInteraction.assetSelectionStart.id); | ||||||
|       let endBucketIndex = $assetStore.getBucketIndexByAssetId(asset.id); |       let endBucketIndex = $assetStore.getBucketIndexByAssetId(asset.id); | ||||||
| 
 | 
 | ||||||
|       if (startBucketIndex === null || endBucketIndex === null) { |       if (startBucketIndex === null || endBucketIndex === null) { | ||||||
| @ -667,7 +665,7 @@ | |||||||
|         await $assetStore.loadBucket(bucket.bucketDate); |         await $assetStore.loadBucket(bucket.bucketDate); | ||||||
|         for (const asset of bucket.assets) { |         for (const asset of bucket.assets) { | ||||||
|           if (deselect) { |           if (deselect) { | ||||||
|             assetInteractionStore.removeAssetFromMultiselectGroup(asset); |             assetInteraction.removeAssetFromMultiselectGroup(asset); | ||||||
|           } else { |           } else { | ||||||
|             handleSelectAsset(asset); |             handleSelectAsset(asset); | ||||||
|           } |           } | ||||||
| @ -682,16 +680,16 @@ | |||||||
|         const assetsGroupByDate = splitBucketIntoDateGroups(bucket, $locale); |         const assetsGroupByDate = splitBucketIntoDateGroups(bucket, $locale); | ||||||
|         for (const dateGroup of assetsGroupByDate) { |         for (const dateGroup of assetsGroupByDate) { | ||||||
|           const dateGroupTitle = formatGroupTitle(dateGroup.date); |           const dateGroupTitle = formatGroupTitle(dateGroup.date); | ||||||
|           if (dateGroup.assets.every((a) => $selectedAssets.has(a))) { |           if (dateGroup.assets.every((a) => assetInteraction.selectedAssets.has(a))) { | ||||||
|             assetInteractionStore.addGroupToMultiselectGroup(dateGroupTitle); |             assetInteraction.addGroupToMultiselectGroup(dateGroupTitle); | ||||||
|           } else { |           } else { | ||||||
|             assetInteractionStore.removeGroupFromMultiselectGroup(dateGroupTitle); |             assetInteraction.removeGroupFromMultiselectGroup(dateGroupTitle); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     assetInteractionStore.setAssetSelectionStart(deselect ? null : asset); |     assetInteraction.setAssetSelectionStart(deselect ? null : asset); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const selectAssetCandidates = (endAsset: AssetResponseDto) => { |   const selectAssetCandidates = (endAsset: AssetResponseDto) => { | ||||||
| @ -699,7 +697,7 @@ | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const startAsset = $assetSelectionStart; |     const startAsset = assetInteraction.assetSelectionStart; | ||||||
|     if (!startAsset) { |     if (!startAsset) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @ -711,11 +709,11 @@ | |||||||
|       [start, end] = [end, start]; |       [start, end] = [end, start]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     assetInteractionStore.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1)); |     assetInteraction.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1)); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const onSelectStart = (e: Event) => { |   const onSelectStart = (e: Event) => { | ||||||
|     if ($isMultiSelectState && shiftKeyIsDown) { |     if (assetInteraction.selectionActive && shiftKeyIsDown) { | ||||||
|       e.preventDefault(); |       e.preventDefault(); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| @ -724,12 +722,11 @@ | |||||||
|   }); |   }); | ||||||
|   let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); |   let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); | ||||||
|   let isEmpty = $derived($assetStore.initialized && $assetStore.buckets.length === 0); |   let isEmpty = $derived($assetStore.initialized && $assetStore.buckets.length === 0); | ||||||
|   let idsSelectedAssets = $derived([...$selectedAssets].map(({ id }) => id)); |   let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id)); | ||||||
|   let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); |  | ||||||
| 
 | 
 | ||||||
|   $effect(() => { |   $effect(() => { | ||||||
|     if (isEmpty) { |     if (isEmpty) { | ||||||
|       assetInteractionStore.clearMultiselect(); |       assetInteraction.clearMultiselect(); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| @ -760,12 +757,12 @@ | |||||||
|         { shortcut: { key: 'Escape' }, onShortcut: onEscape }, |         { shortcut: { key: 'Escape' }, onShortcut: onEscape }, | ||||||
|         { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) }, |         { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) }, | ||||||
|         { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, |         { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, | ||||||
|         { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteractionStore) }, |         { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteraction) }, | ||||||
|         { shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement }, |         { shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement }, | ||||||
|         { shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement }, |         { shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement }, | ||||||
|       ]; |       ]; | ||||||
| 
 | 
 | ||||||
|       if ($isMultiSelectState) { |       if (assetInteraction.selectionActive) { | ||||||
|         shortcuts.push( |         shortcuts.push( | ||||||
|           { shortcut: { key: 'Delete' }, onShortcut: onDelete }, |           { shortcut: { key: 'Delete' }, onShortcut: onDelete }, | ||||||
|           { shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete }, |           { shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete }, | ||||||
| @ -781,13 +778,13 @@ | |||||||
| 
 | 
 | ||||||
|   $effect(() => { |   $effect(() => { | ||||||
|     if (!lastAssetMouseEvent) { |     if (!lastAssetMouseEvent) { | ||||||
|       assetInteractionStore.clearAssetSelectionCandidates(); |       assetInteraction.clearAssetSelectionCandidates(); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   $effect(() => { |   $effect(() => { | ||||||
|     if (!shiftKeyIsDown) { |     if (!shiftKeyIsDown) { | ||||||
|       assetInteractionStore.clearAssetSelectionCandidates(); |       assetInteraction.clearAssetSelectionCandidates(); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| @ -889,7 +886,7 @@ | |||||||
|             {withStacked} |             {withStacked} | ||||||
|             {showArchiveIcon} |             {showArchiveIcon} | ||||||
|             {assetStore} |             {assetStore} | ||||||
|             {assetInteractionStore} |             {assetInteraction} | ||||||
|             {isSelectionMode} |             {isSelectionMode} | ||||||
|             {singleSelect} |             {singleSelect} | ||||||
|             {onScrollTarget} |             {onScrollTarget} | ||||||
|  | |||||||
| @ -15,11 +15,11 @@ | |||||||
|   import ControlAppBar from '../shared-components/control-app-bar.svelte'; |   import ControlAppBar from '../shared-components/control-app-bar.svelte'; | ||||||
|   import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; |   import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; | ||||||
|   import { cancelMultiselect } from '$lib/utils/asset-utils'; |   import { cancelMultiselect } from '$lib/utils/asset-utils'; | ||||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte'; |   import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte'; | ||||||
|   import { NotificationType, notificationController } from '../shared-components/notification/notification'; |   import { NotificationType, notificationController } from '../shared-components/notification/notification'; | ||||||
|   import type { Viewport } from '$lib/stores/assets.store'; |   import type { Viewport } from '$lib/stores/assets.store'; | ||||||
|   import { t } from 'svelte-i18n'; |   import { t } from 'svelte-i18n'; | ||||||
|  |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     sharedLink: SharedLinkResponseDto; |     sharedLink: SharedLinkResponseDto; | ||||||
| @ -29,12 +29,10 @@ | |||||||
|   let { sharedLink = $bindable(), isOwned }: Props = $props(); |   let { sharedLink = $bindable(), isOwned }: Props = $props(); | ||||||
| 
 | 
 | ||||||
|   const viewport: Viewport = $state({ width: 0, height: 0 }); |   const viewport: Viewport = $state({ width: 0, height: 0 }); | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
|   const { selectedAssets } = assetInteractionStore; |  | ||||||
|   let innerWidth: number = $state(0); |   let innerWidth: number = $state(0); | ||||||
| 
 | 
 | ||||||
|   let assets = $derived(sharedLink.assets); |   let assets = $derived(sharedLink.assets); | ||||||
|   let isMultiSelectionMode = $derived($selectedAssets.size > 0); |  | ||||||
| 
 | 
 | ||||||
|   dragAndDropFilesStore.subscribe((value) => { |   dragAndDropFilesStore.subscribe((value) => { | ||||||
|     if (value.isDragging && value.files.length > 0) { |     if (value.isDragging && value.files.length > 0) { | ||||||
| @ -73,15 +71,18 @@ | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleSelectAll = () => { |   const handleSelectAll = () => { | ||||||
|     assetInteractionStore.selectAssets(assets); |     assetInteraction.selectAssets(assets); | ||||||
|   }; |   }; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <svelte:window bind:innerWidth /> | <svelte:window bind:innerWidth /> | ||||||
| 
 | 
 | ||||||
| <section class="bg-immich-bg dark:bg-immich-dark-bg"> | <section class="bg-immich-bg dark:bg-immich-dark-bg"> | ||||||
|   {#if isMultiSelectionMode} |   {#if assetInteraction.selectionActive} | ||||||
|     <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => cancelMultiselect(assetInteractionStore)}> |     <AssetSelectControlBar | ||||||
|  |       assets={assetInteraction.selectedAssets} | ||||||
|  |       clearSelect={() => cancelMultiselect(assetInteraction)} | ||||||
|  |     > | ||||||
|       <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} /> |       <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} /> | ||||||
|       {#if sharedLink?.allowDownload} |       {#if sharedLink?.allowDownload} | ||||||
|         <DownloadAction filename="immich-shared.zip" /> |         <DownloadAction filename="immich-shared.zip" /> | ||||||
| @ -112,6 +113,6 @@ | |||||||
|     </ControlAppBar> |     </ControlAppBar> | ||||||
|   {/if} |   {/if} | ||||||
|   <section class="my-[160px] mx-4" bind:clientHeight={viewport.height} bind:clientWidth={viewport.width}> |   <section class="my-[160px] mx-4" bind:clientHeight={viewport.height} bind:clientWidth={viewport.width}> | ||||||
|     <GalleryViewer {assets} {assetInteractionStore} {viewport} /> |     <GalleryViewer {assets} {assetInteraction} {viewport} /> | ||||||
|   </section> |   </section> | ||||||
| </section> | </section> | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ | |||||||
|   import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; |   import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; | ||||||
|   import { AppRoute, AssetAction } from '$lib/constants'; |   import { AppRoute, AssetAction } from '$lib/constants'; | ||||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; |   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||||
|   import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import type { Viewport } from '$lib/stores/assets.store'; |   import type { Viewport } from '$lib/stores/assets.store'; | ||||||
|   import { showDeleteModal } from '$lib/stores/preferences.store'; |   import { showDeleteModal } from '$lib/stores/preferences.store'; | ||||||
|   import { deleteAssets } from '$lib/utils/actions'; |   import { deleteAssets } from '$lib/utils/actions'; | ||||||
| @ -22,10 +21,11 @@ | |||||||
|   import Portal from '../portal/portal.svelte'; |   import Portal from '../portal/portal.svelte'; | ||||||
|   import { handlePromiseError } from '$lib/utils'; |   import { handlePromiseError } from '$lib/utils'; | ||||||
|   import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte'; |   import DeleteAssetDialog from '../../photos-page/delete-asset-dialog.svelte'; | ||||||
|  |   import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     assets: AssetResponseDto[]; |     assets: AssetResponseDto[]; | ||||||
|     assetInteractionStore: AssetInteractionStore; |     assetInteraction: AssetInteraction; | ||||||
|     disableAssetSelect?: boolean; |     disableAssetSelect?: boolean; | ||||||
|     showArchiveIcon?: boolean; |     showArchiveIcon?: boolean; | ||||||
|     viewport: Viewport; |     viewport: Viewport; | ||||||
| @ -38,7 +38,7 @@ | |||||||
| 
 | 
 | ||||||
|   let { |   let { | ||||||
|     assets = $bindable(), |     assets = $bindable(), | ||||||
|     assetInteractionStore = $bindable(), |     assetInteraction, | ||||||
|     disableAssetSelect = false, |     disableAssetSelect = false, | ||||||
|     showArchiveIcon = false, |     showArchiveIcon = false, | ||||||
|     viewport, |     viewport, | ||||||
| @ -51,11 +51,8 @@ | |||||||
| 
 | 
 | ||||||
|   let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore; |   let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore; | ||||||
| 
 | 
 | ||||||
|   const { assetSelectionCandidates, assetSelectionStart, selectedAssets, isMultiSelectState } = assetInteractionStore; |  | ||||||
| 
 |  | ||||||
|   let showShortcuts = $state(false); |   let showShortcuts = $state(false); | ||||||
|   let currentViewAssetIndex = 0; |   let currentViewAssetIndex = 0; | ||||||
|   let isMultiSelectionMode = $derived($selectedAssets.size > 0); |  | ||||||
|   let shiftKeyIsDown = $state(false); |   let shiftKeyIsDown = $state(false); | ||||||
|   let lastAssetMouseEvent: AssetResponseDto | null = $state(null); |   let lastAssetMouseEvent: AssetResponseDto | null = $state(null); | ||||||
| 
 | 
 | ||||||
| @ -66,11 +63,11 @@ | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const selectAllAssets = () => { |   const selectAllAssets = () => { | ||||||
|     assetInteractionStore.selectAssets(assets); |     assetInteraction.selectAssets(assets); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const deselectAllAssets = () => { |   const deselectAllAssets = () => { | ||||||
|     cancelMultiselect(assetInteractionStore); |     cancelMultiselect(assetInteraction); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const onKeyDown = (event: KeyboardEvent) => { |   const onKeyDown = (event: KeyboardEvent) => { | ||||||
| @ -91,23 +88,23 @@ | |||||||
|     if (!asset) { |     if (!asset) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const deselect = $selectedAssets.has(asset); |     const deselect = assetInteraction.selectedAssets.has(asset); | ||||||
| 
 | 
 | ||||||
|     // Select/deselect already loaded assets |     // Select/deselect already loaded assets | ||||||
|     if (deselect) { |     if (deselect) { | ||||||
|       for (const candidate of $assetSelectionCandidates || []) { |       for (const candidate of assetInteraction.assetSelectionCandidates) { | ||||||
|         assetInteractionStore.removeAssetFromMultiselectGroup(candidate); |         assetInteraction.removeAssetFromMultiselectGroup(candidate); | ||||||
|       } |       } | ||||||
|       assetInteractionStore.removeAssetFromMultiselectGroup(asset); |       assetInteraction.removeAssetFromMultiselectGroup(asset); | ||||||
|     } else { |     } else { | ||||||
|       for (const candidate of $assetSelectionCandidates || []) { |       for (const candidate of assetInteraction.assetSelectionCandidates) { | ||||||
|         assetInteractionStore.selectAsset(candidate); |         assetInteraction.selectAsset(candidate); | ||||||
|       } |       } | ||||||
|       assetInteractionStore.selectAsset(asset); |       assetInteraction.selectAsset(asset); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     assetInteractionStore.clearAssetSelectionCandidates(); |     assetInteraction.clearAssetSelectionCandidates(); | ||||||
|     assetInteractionStore.setAssetSelectionStart(deselect ? null : asset); |     assetInteraction.setAssetSelectionStart(deselect ? null : asset); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => { |   const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => { | ||||||
| @ -122,7 +119,7 @@ | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const startAsset = $assetSelectionStart; |     const startAsset = assetInteraction.assetSelectionStart; | ||||||
|     if (!startAsset) { |     if (!startAsset) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @ -134,17 +131,17 @@ | |||||||
|       [start, end] = [end, start]; |       [start, end] = [end, start]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     assetInteractionStore.setAssetSelectionCandidates(assets.slice(start, end + 1)); |     assetInteraction.setAssetSelectionCandidates(assets.slice(start, end + 1)); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const onSelectStart = (e: Event) => { |   const onSelectStart = (e: Event) => { | ||||||
|     if ($isMultiSelectState && shiftKeyIsDown) { |     if (assetInteraction.selectionActive && shiftKeyIsDown) { | ||||||
|       e.preventDefault(); |       e.preventDefault(); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const onDelete = () => { |   const onDelete = () => { | ||||||
|     const hasTrashedAsset = Array.from($selectedAssets).some((asset) => asset.isTrashed); |     const hasTrashedAsset = assetInteraction.selectedAssetsArray.some((asset) => asset.isTrashed); | ||||||
| 
 | 
 | ||||||
|     if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) { |     if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) { | ||||||
|       isShowDeleteConfirmation = true; |       isShowDeleteConfirmation = true; | ||||||
| @ -168,11 +165,11 @@ | |||||||
|       (assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))), |       (assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))), | ||||||
|       idsSelectedAssets, |       idsSelectedAssets, | ||||||
|     ); |     ); | ||||||
|     assetInteractionStore.clearMultiselect(); |     assetInteraction.clearMultiselect(); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const toggleArchive = async () => { |   const toggleArchive = async () => { | ||||||
|     const ids = await archiveAssets(Array.from($selectedAssets), !isAllArchived); |     const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived); | ||||||
|     if (ids) { |     if (ids) { | ||||||
|       assets.filter((asset) => !ids.includes(asset.id)); |       assets.filter((asset) => !ids.includes(asset.id)); | ||||||
|       deselectAllAssets(); |       deselectAllAssets(); | ||||||
| @ -191,7 +188,7 @@ | |||||||
|         { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets() }, |         { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets() }, | ||||||
|       ]; |       ]; | ||||||
| 
 | 
 | ||||||
|       if ($isMultiSelectState) { |       if (assetInteraction.selectionActive) { | ||||||
|         shortcuts.push( |         shortcuts.push( | ||||||
|           { shortcut: { key: 'Escape' }, onShortcut: deselectAllAssets }, |           { shortcut: { key: 'Escape' }, onShortcut: deselectAllAssets }, | ||||||
|           { shortcut: { key: 'Delete' }, onShortcut: onDelete }, |           { shortcut: { key: 'Delete' }, onShortcut: onDelete }, | ||||||
| @ -266,14 +263,13 @@ | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const assetMouseEventHandler = (asset: AssetResponseDto | null) => { |   const assetMouseEventHandler = (asset: AssetResponseDto | null) => { | ||||||
|     if ($isMultiSelectState) { |     if (assetInteraction.selectionActive) { | ||||||
|       handleSelectAssetCandidates(asset); |       handleSelectAssetCandidates(asset); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); |   let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); | ||||||
|   let idsSelectedAssets = $derived([...$selectedAssets].map(({ id }) => id)); |   let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id)); | ||||||
|   let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); |  | ||||||
| 
 | 
 | ||||||
|   let geometry = $derived( |   let geometry = $derived( | ||||||
|     (() => { |     (() => { | ||||||
| @ -297,13 +293,13 @@ | |||||||
| 
 | 
 | ||||||
|   $effect(() => { |   $effect(() => { | ||||||
|     if (!lastAssetMouseEvent) { |     if (!lastAssetMouseEvent) { | ||||||
|       assetInteractionStore.clearAssetSelectionCandidates(); |       assetInteraction.clearAssetSelectionCandidates(); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   $effect(() => { |   $effect(() => { | ||||||
|     if (!shiftKeyIsDown) { |     if (!shiftKeyIsDown) { | ||||||
|       assetInteractionStore.clearAssetSelectionCandidates(); |       assetInteraction.clearAssetSelectionCandidates(); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| @ -318,7 +314,7 @@ | |||||||
| 
 | 
 | ||||||
| {#if isShowDeleteConfirmation} | {#if isShowDeleteConfirmation} | ||||||
|   <DeleteAssetDialog |   <DeleteAssetDialog | ||||||
|     size={idsSelectedAssets.length} |     size={assetInteraction.selectedAssets.size} | ||||||
|     onCancel={() => (isShowDeleteConfirmation = false)} |     onCancel={() => (isShowDeleteConfirmation = false)} | ||||||
|     onConfirm={() => handlePromiseError(trashOrDelete(true))} |     onConfirm={() => handlePromiseError(trashOrDelete(true))} | ||||||
|   /> |   /> | ||||||
| @ -340,7 +336,7 @@ | |||||||
|         <Thumbnail |         <Thumbnail | ||||||
|           readonly={disableAssetSelect} |           readonly={disableAssetSelect} | ||||||
|           onClick={(asset) => { |           onClick={(asset) => { | ||||||
|             if (isMultiSelectionMode) { |             if (assetInteraction.selectionActive) { | ||||||
|               handleSelectAssets(asset); |               handleSelectAssets(asset); | ||||||
|               return; |               return; | ||||||
|             } |             } | ||||||
| @ -351,8 +347,8 @@ | |||||||
|           onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)} |           onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)} | ||||||
|           {showArchiveIcon} |           {showArchiveIcon} | ||||||
|           {asset} |           {asset} | ||||||
|           selected={$selectedAssets.has(asset)} |           selected={assetInteraction.selectedAssets.has(asset)} | ||||||
|           selectionCandidate={$assetSelectionCandidates.has(asset)} |           selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)} | ||||||
|           thumbnailWidth={geometry.boxes[i].width} |           thumbnailWidth={geometry.boxes[i].width} | ||||||
|           thumbnailHeight={geometry.boxes[i].height} |           thumbnailHeight={geometry.boxes[i].height} | ||||||
|         /> |         /> | ||||||
|  | |||||||
| @ -1,86 +0,0 @@ | |||||||
| import type { AssetResponseDto } from '@immich/sdk'; |  | ||||||
| import { derived, readonly, writable } from 'svelte/store'; |  | ||||||
| 
 |  | ||||||
| export type AssetInteractionStore = ReturnType<typeof createAssetInteractionStore>; |  | ||||||
| 
 |  | ||||||
| export function createAssetInteractionStore() { |  | ||||||
|   const selectedAssets = writable(new Set<AssetResponseDto>()); |  | ||||||
|   const selectedGroup = writable(new Set<string>()); |  | ||||||
|   const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0); |  | ||||||
| 
 |  | ||||||
|   // Candidates for the range selection. This set includes only loaded assets, so it improves highlight
 |  | ||||||
|   // performance. From the user's perspective, range is highlighted almost immediately
 |  | ||||||
|   const assetSelectionCandidates = writable(new Set<AssetResponseDto>()); |  | ||||||
|   // The beginning of the selection range
 |  | ||||||
|   const assetSelectionStart = writable<AssetResponseDto | null>(null); |  | ||||||
| 
 |  | ||||||
|   const selectAsset = (asset: AssetResponseDto) => { |  | ||||||
|     selectedAssets.update(($selectedAssets) => $selectedAssets.add(asset)); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const selectAssets = (assets: AssetResponseDto[]) => { |  | ||||||
|     selectedAssets.update(($selectedAssets) => { |  | ||||||
|       for (const asset of assets) { |  | ||||||
|         $selectedAssets.add(asset); |  | ||||||
|       } |  | ||||||
|       return $selectedAssets; |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const removeAssetFromMultiselectGroup = (asset: AssetResponseDto) => { |  | ||||||
|     selectedAssets.update(($selectedAssets) => { |  | ||||||
|       $selectedAssets.delete(asset); |  | ||||||
|       return $selectedAssets; |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const addGroupToMultiselectGroup = (group: string) => { |  | ||||||
|     selectedGroup.update(($selectedGroup) => $selectedGroup.add(group)); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const removeGroupFromMultiselectGroup = (group: string) => { |  | ||||||
|     selectedGroup.update(($selectedGroup) => { |  | ||||||
|       $selectedGroup.delete(group); |  | ||||||
|       return $selectedGroup; |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const setAssetSelectionStart = (asset: AssetResponseDto | null) => { |  | ||||||
|     assetSelectionStart.set(asset); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const setAssetSelectionCandidates = (assets: AssetResponseDto[]) => { |  | ||||||
|     assetSelectionCandidates.set(new Set(assets)); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const clearAssetSelectionCandidates = () => { |  | ||||||
|     assetSelectionCandidates.set(new Set()); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const clearMultiselect = () => { |  | ||||||
|     // Multi-selection
 |  | ||||||
|     selectedAssets.set(new Set()); |  | ||||||
|     selectedGroup.set(new Set()); |  | ||||||
| 
 |  | ||||||
|     // Range selection
 |  | ||||||
|     assetSelectionCandidates.set(new Set()); |  | ||||||
|     assetSelectionStart.set(null); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     selectAsset, |  | ||||||
|     selectAssets, |  | ||||||
|     removeAssetFromMultiselectGroup, |  | ||||||
|     addGroupToMultiselectGroup, |  | ||||||
|     removeGroupFromMultiselectGroup, |  | ||||||
|     setAssetSelectionCandidates, |  | ||||||
|     clearAssetSelectionCandidates, |  | ||||||
|     setAssetSelectionStart, |  | ||||||
|     clearMultiselect, |  | ||||||
|     isMultiSelectState: readonly(isMultiSelectStoreState), |  | ||||||
|     selectedAssets: readonly(selectedAssets), |  | ||||||
|     selectedGroup: readonly(selectedGroup), |  | ||||||
|     assetSelectionCandidates: readonly(assetSelectionCandidates), |  | ||||||
|     assetSelectionStart: readonly(assetSelectionStart), |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
							
								
								
									
										40
									
								
								web/src/lib/stores/asset-interaction.svelte.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								web/src/lib/stores/asset-interaction.svelte.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
|  | import { resetSavedUser, user } from '$lib/stores/user.store'; | ||||||
|  | import { assetFactory } from '@test-data/factories/asset-factory'; | ||||||
|  | import { userAdminFactory } from '@test-data/factories/user-factory'; | ||||||
|  | 
 | ||||||
|  | describe('AssetInteraction', () => { | ||||||
|  |   let assetInteraction: AssetInteraction; | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     assetInteraction = new AssetInteraction(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('calculates derived values from selection', () => { | ||||||
|  |     assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: true, isTrashed: true })); | ||||||
|  |     assetInteraction.selectAsset(assetFactory.build({ isFavorite: true, isArchived: false, isTrashed: false })); | ||||||
|  | 
 | ||||||
|  |     expect(assetInteraction.selectionActive).toBe(true); | ||||||
|  |     expect(assetInteraction.isAllTrashed).toBe(false); | ||||||
|  |     expect(assetInteraction.isAllArchived).toBe(false); | ||||||
|  |     expect(assetInteraction.isAllFavorite).toBe(true); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('updates isAllUserOwned when the active user changes', () => { | ||||||
|  |     const [user1, user2] = userAdminFactory.buildList(2); | ||||||
|  |     assetInteraction.selectAsset(assetFactory.build({ ownerId: user1.id })); | ||||||
|  | 
 | ||||||
|  |     const cleanup = $effect.root(() => { | ||||||
|  |       expect(assetInteraction.isAllUserOwned).toBe(false); | ||||||
|  | 
 | ||||||
|  |       user.set(user1); | ||||||
|  |       expect(assetInteraction.isAllUserOwned).toBe(true); | ||||||
|  | 
 | ||||||
|  |       user.set(user2); | ||||||
|  |       expect(assetInteraction.isAllUserOwned).toBe(false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     cleanup(); | ||||||
|  |     resetSavedUser(); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										66
									
								
								web/src/lib/stores/asset-interaction.svelte.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								web/src/lib/stores/asset-interaction.svelte.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | |||||||
|  | import { user } from '$lib/stores/user.store'; | ||||||
|  | import type { AssetResponseDto, UserAdminResponseDto } from '@immich/sdk'; | ||||||
|  | import { SvelteSet } from 'svelte/reactivity'; | ||||||
|  | import { fromStore } from 'svelte/store'; | ||||||
|  | 
 | ||||||
|  | export class AssetInteraction { | ||||||
|  |   readonly selectedAssets = new SvelteSet<AssetResponseDto>(); | ||||||
|  |   readonly selectedGroup = new SvelteSet<string>(); | ||||||
|  |   assetSelectionCandidates = $state(new SvelteSet<AssetResponseDto>()); | ||||||
|  |   assetSelectionStart = $state<AssetResponseDto | null>(null); | ||||||
|  | 
 | ||||||
|  |   selectionActive = $derived(this.selectedAssets.size > 0); | ||||||
|  |   selectedAssetsArray = $derived([...this.selectedAssets]); | ||||||
|  | 
 | ||||||
|  |   private user = fromStore<UserAdminResponseDto | undefined>(user); | ||||||
|  |   private userId = $derived(this.user.current?.id); | ||||||
|  | 
 | ||||||
|  |   isAllTrashed = $derived(this.selectedAssetsArray.every((asset) => asset.isTrashed)); | ||||||
|  |   isAllArchived = $derived(this.selectedAssetsArray.every((asset) => asset.isArchived)); | ||||||
|  |   isAllFavorite = $derived(this.selectedAssetsArray.every((asset) => asset.isFavorite)); | ||||||
|  |   isAllUserOwned = $derived(this.selectedAssetsArray.every((asset) => asset.ownerId === this.userId)); | ||||||
|  | 
 | ||||||
|  |   selectAsset(asset: AssetResponseDto) { | ||||||
|  |     this.selectedAssets.add(asset); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   selectAssets(assets: AssetResponseDto[]) { | ||||||
|  |     for (const asset of assets) { | ||||||
|  |       this.selectedAssets.add(asset); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   removeAssetFromMultiselectGroup(asset: AssetResponseDto) { | ||||||
|  |     this.selectedAssets.delete(asset); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   addGroupToMultiselectGroup(group: string) { | ||||||
|  |     this.selectedGroup.add(group); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   removeGroupFromMultiselectGroup(group: string) { | ||||||
|  |     this.selectedGroup.delete(group); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   setAssetSelectionStart(asset: AssetResponseDto | null) { | ||||||
|  |     this.assetSelectionStart = asset; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   setAssetSelectionCandidates(assets: AssetResponseDto[]) { | ||||||
|  |     this.assetSelectionCandidates = new SvelteSet(assets); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   clearAssetSelectionCandidates() { | ||||||
|  |     this.assetSelectionCandidates.clear(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   clearMultiselect() { | ||||||
|  |     // Multi-selection
 | ||||||
|  |     this.selectedAssets.clear(); | ||||||
|  |     this.selectedGroup.clear(); | ||||||
|  | 
 | ||||||
|  |     // Range selection
 | ||||||
|  |     this.assetSelectionCandidates.clear(); | ||||||
|  |     this.assetSelectionStart = null; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -2,7 +2,7 @@ import { goto } from '$app/navigation'; | |||||||
| import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte'; | import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte'; | ||||||
| import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification'; | import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification'; | ||||||
| import { AppRoute } from '$lib/constants'; | import { AppRoute } from '$lib/constants'; | ||||||
| import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; | import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||||
| import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; | import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; | ||||||
| import { downloadManager } from '$lib/stores/download'; | import { downloadManager } from '$lib/stores/download'; | ||||||
| @ -460,7 +460,7 @@ export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: S | |||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => { | export const selectAllAssets = async (assetStore: AssetStore, assetInteraction: AssetInteraction) => { | ||||||
|   if (get(isSelectingAllAssets)) { |   if (get(isSelectingAllAssets)) { | ||||||
|     // Selection is already ongoing
 |     // Selection is already ongoing
 | ||||||
|     return; |     return; | ||||||
| @ -474,7 +474,7 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt | |||||||
|       if (!get(isSelectingAllAssets)) { |       if (!get(isSelectingAllAssets)) { | ||||||
|         break; // Cancelled
 |         break; // Cancelled
 | ||||||
|       } |       } | ||||||
|       assetInteractionStore.selectAssets(bucket.assets); |       assetInteraction.selectAssets(bucket.assets); | ||||||
| 
 | 
 | ||||||
|       // We use setTimeout to allow the UI to update. Otherwise, this may
 |       // We use setTimeout to allow the UI to update. Otherwise, this may
 | ||||||
|       // cause a long delay between the start of 'select all' and the
 |       // cause a long delay between the start of 'select all' and the
 | ||||||
| @ -489,9 +489,9 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt | |||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const cancelMultiselect = (assetInteractionStore: AssetInteractionStore) => { | export const cancelMultiselect = (assetInteraction: AssetInteraction) => { | ||||||
|   isSelectingAllAssets.set(false); |   isSelectingAllAssets.set(false); | ||||||
|   assetInteractionStore.clearMultiselect(); |   assetInteraction.clearMultiselect(); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const toggleArchive = async (asset: AssetResponseDto) => { | export const toggleArchive = async (asset: AssetResponseDto) => { | ||||||
|  | |||||||
| @ -35,7 +35,6 @@ | |||||||
|   import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; |   import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; | ||||||
|   import { AppRoute, AlbumPageViewMode } from '$lib/constants'; |   import { AppRoute, AlbumPageViewMode } from '$lib/constants'; | ||||||
|   import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store'; |   import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store'; | ||||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; |   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||||
|   import { AssetStore } from '$lib/stores/assets.store'; |   import { AssetStore } from '$lib/stores/assets.store'; | ||||||
|   import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; |   import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; | ||||||
| @ -87,6 +86,7 @@ | |||||||
|   import { onDestroy } from 'svelte'; |   import { onDestroy } from 'svelte'; | ||||||
|   import { confirmAlbumDelete } from '$lib/utils/album-utils'; |   import { confirmAlbumDelete } from '$lib/utils/album-utils'; | ||||||
|   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; |   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; | ||||||
|  |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     data: PageData; |     data: PageData; | ||||||
| @ -107,11 +107,8 @@ | |||||||
|   let reactions: ActivityResponseDto[] = $state([]); |   let reactions: ActivityResponseDto[] = $state([]); | ||||||
|   let albumOrder: AssetOrder | undefined = $state(data.album.order); |   let albumOrder: AssetOrder | undefined = $state(data.album.order); | ||||||
| 
 | 
 | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
|   const { isMultiSelectState, selectedAssets } = assetInteractionStore; |   const timelineInteraction = new AssetInteraction(); | ||||||
| 
 |  | ||||||
|   const timelineInteractionStore = createAssetInteractionStore(); |  | ||||||
|   const { selectedAssets: timelineSelected } = timelineInteractionStore; |  | ||||||
| 
 | 
 | ||||||
|   afterNavigate(({ from }) => { |   afterNavigate(({ from }) => { | ||||||
|     let url: string | undefined = from?.url?.pathname; |     let url: string | undefined = from?.url?.pathname; | ||||||
| @ -234,8 +231,8 @@ | |||||||
|     if ($showAssetViewer) { |     if ($showAssetViewer) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     if ($isMultiSelectState) { |     if (assetInteraction.selectionActive) { | ||||||
|       cancelMultiselect(assetInteractionStore); |       cancelMultiselect(assetInteraction); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     await goto(backUrl); |     await goto(backUrl); | ||||||
| @ -245,9 +242,8 @@ | |||||||
|   const refreshAlbum = async () => { |   const refreshAlbum = async () => { | ||||||
|     album = await getAlbumInfo({ id: album.id, withoutAssets: true }); |     album = await getAlbumInfo({ id: album.id, withoutAssets: true }); | ||||||
|   }; |   }; | ||||||
| 
 |  | ||||||
|   const handleAddAssets = async () => { |   const handleAddAssets = async () => { | ||||||
|     const assetIds = [...$timelineSelected].map((asset) => asset.id); |     const assetIds = timelineInteraction.selectedAssetsArray.map((asset) => asset.id); | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|       const results = await addAssetsToAlbum({ |       const results = await addAssetsToAlbum({ | ||||||
| @ -263,7 +259,7 @@ | |||||||
| 
 | 
 | ||||||
|       await refreshAlbum(); |       await refreshAlbum(); | ||||||
| 
 | 
 | ||||||
|       timelineInteractionStore.clearMultiselect(); |       timelineInteraction.clearMultiselect(); | ||||||
|       await setModeToView(); |       await setModeToView(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       handleError(error, $t('errors.error_adding_assets_to_album')); |       handleError(error, $t('errors.error_adding_assets_to_album')); | ||||||
| @ -284,13 +280,13 @@ | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleCloseSelectAssets = async () => { |   const handleCloseSelectAssets = async () => { | ||||||
|     timelineInteractionStore.clearMultiselect(); |     timelineInteraction.clearMultiselect(); | ||||||
|     await setModeToView(); |     await setModeToView(); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleSelectFromComputer = async () => { |   const handleSelectFromComputer = async () => { | ||||||
|     await openFileUploadDialog({ albumId: album.id }); |     await openFileUploadDialog({ albumId: album.id }); | ||||||
|     timelineInteractionStore.clearMultiselect(); |     timelineInteraction.clearMultiselect(); | ||||||
|     await setModeToView(); |     await setModeToView(); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
| @ -359,16 +355,16 @@ | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     viewMode = AlbumPageViewMode.VIEW; |     viewMode = AlbumPageViewMode.VIEW; | ||||||
|     assetInteractionStore.clearMultiselect(); |     assetInteraction.clearMultiselect(); | ||||||
| 
 | 
 | ||||||
|     await updateThumbnail(assetId); |     await updateThumbnail(assetId); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const updateThumbnailUsingCurrentSelection = async () => { |   const updateThumbnailUsingCurrentSelection = async () => { | ||||||
|     if ($selectedAssets.size === 1) { |     if (assetInteraction.selectedAssets.size === 1) { | ||||||
|       const assetId = [...$selectedAssets][0].id; |       const [firstAsset] = assetInteraction.selectedAssets; | ||||||
|       assetInteractionStore.clearMultiselect(); |       assetInteraction.clearMultiselect(); | ||||||
|       await updateThumbnail(assetId); |       await updateThumbnail(firstAsset.id); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
| @ -410,9 +406,6 @@ | |||||||
|   let timelineStore = $derived(new AssetStore({ isArchived: false, withPartners: true }, albumId)); |   let timelineStore = $derived(new AssetStore({ isArchived: false, withPartners: true }, albumId)); | ||||||
| 
 | 
 | ||||||
|   let isOwned = $derived($user.id == album.ownerId); |   let isOwned = $derived($user.id == album.ownerId); | ||||||
|   let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); |  | ||||||
|   let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); |  | ||||||
|   let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); |  | ||||||
| 
 | 
 | ||||||
|   let showActivityStatus = $derived( |   let showActivityStatus = $derived( | ||||||
|     album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0), |     album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0), | ||||||
| @ -433,40 +426,50 @@ | |||||||
| 
 | 
 | ||||||
| <div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}> | <div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}> | ||||||
|   <div class="relative w-full shrink"> |   <div class="relative w-full shrink"> | ||||||
|     {#if $isMultiSelectState} |     {#if assetInteraction.selectionActive} | ||||||
|       <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> |       <AssetSelectControlBar | ||||||
|  |         assets={assetInteraction.selectedAssets} | ||||||
|  |         clearSelect={() => assetInteraction.clearMultiselect()} | ||||||
|  |       > | ||||||
|         <CreateSharedLink /> |         <CreateSharedLink /> | ||||||
|         <SelectAllAssets {assetStore} {assetInteractionStore} /> |         <SelectAllAssets {assetStore} {assetInteraction} /> | ||||||
|         <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> |         <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> | ||||||
|           <AddToAlbum /> |           <AddToAlbum /> | ||||||
|           <AddToAlbum shared /> |           <AddToAlbum shared /> | ||||||
|         </ButtonContextMenu> |         </ButtonContextMenu> | ||||||
|         {#if isAllUserOwned} |         {#if assetInteraction.isAllUserOwned} | ||||||
|           <FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> |           <FavoriteAction | ||||||
|  |             removeFavorite={assetInteraction.isAllFavorite} | ||||||
|  |             onFavorite={() => assetStore.triggerUpdate()} | ||||||
|  |           /> | ||||||
|         {/if} |         {/if} | ||||||
|         <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> |         <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> | ||||||
|           <DownloadAction menuItem filename="{album.albumName}.zip" /> |           <DownloadAction menuItem filename="{album.albumName}.zip" /> | ||||||
|           {#if isAllUserOwned} |           {#if assetInteraction.isAllUserOwned} | ||||||
|             <ChangeDate menuItem /> |             <ChangeDate menuItem /> | ||||||
|             <ChangeLocation menuItem /> |             <ChangeLocation menuItem /> | ||||||
|             {#if $selectedAssets.size === 1} |             {#if assetInteraction.selectedAssets.size === 1} | ||||||
|               <MenuOption |               <MenuOption | ||||||
|                 text={$t('set_as_album_cover')} |                 text={$t('set_as_album_cover')} | ||||||
|                 icon={mdiImageOutline} |                 icon={mdiImageOutline} | ||||||
|                 onClick={() => updateThumbnailUsingCurrentSelection()} |                 onClick={() => updateThumbnailUsingCurrentSelection()} | ||||||
|               /> |               /> | ||||||
|             {/if} |             {/if} | ||||||
|             <ArchiveAction menuItem unarchive={isAllArchived} onArchive={() => assetStore.triggerUpdate()} /> |             <ArchiveAction | ||||||
|  |               menuItem | ||||||
|  |               unarchive={assetInteraction.isAllArchived} | ||||||
|  |               onArchive={() => assetStore.triggerUpdate()} | ||||||
|  |             /> | ||||||
|           {/if} |           {/if} | ||||||
| 
 | 
 | ||||||
|           {#if $preferences.tags.enabled && isAllUserOwned} |           {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} | ||||||
|             <TagAction menuItem /> |             <TagAction menuItem /> | ||||||
|           {/if} |           {/if} | ||||||
| 
 | 
 | ||||||
|           {#if isOwned || isAllUserOwned} |           {#if isOwned || assetInteraction.isAllUserOwned} | ||||||
|             <RemoveFromAlbum menuItem bind:album onRemove={handleRemoveAssets} /> |             <RemoveFromAlbum menuItem bind:album onRemove={handleRemoveAssets} /> | ||||||
|           {/if} |           {/if} | ||||||
|           {#if isAllUserOwned} |           {#if assetInteraction.isAllUserOwned} | ||||||
|             <DeleteAssets menuItem onAssetDelete={handleRemoveAssets} /> |             <DeleteAssets menuItem onAssetDelete={handleRemoveAssets} /> | ||||||
|           {/if} |           {/if} | ||||||
|         </ButtonContextMenu> |         </ButtonContextMenu> | ||||||
| @ -540,10 +543,10 @@ | |||||||
|         <ControlAppBar onClose={handleCloseSelectAssets}> |         <ControlAppBar onClose={handleCloseSelectAssets}> | ||||||
|           {#snippet leading()} |           {#snippet leading()} | ||||||
|             <p class="text-lg dark:text-immich-dark-fg"> |             <p class="text-lg dark:text-immich-dark-fg"> | ||||||
|               {#if $timelineSelected.size === 0} |               {#if !timelineInteraction.selectionActive} | ||||||
|                 {$t('add_to_album')} |                 {$t('add_to_album')} | ||||||
|               {:else} |               {:else} | ||||||
|                 {$t('selected_count', { values: { count: $timelineSelected.size } })} |                 {$t('selected_count', { values: { count: timelineInteraction.selectedAssets.size } })} | ||||||
|               {/if} |               {/if} | ||||||
|             </p> |             </p> | ||||||
|           {/snippet} |           {/snippet} | ||||||
| @ -556,7 +559,7 @@ | |||||||
|             > |             > | ||||||
|               {$t('select_from_computer')} |               {$t('select_from_computer')} | ||||||
|             </button> |             </button> | ||||||
|             <Button size="sm" rounded="lg" disabled={$timelineSelected.size === 0} onclick={handleAddAssets} |             <Button size="sm" rounded="lg" disabled={!timelineInteraction.selectionActive} onclick={handleAddAssets} | ||||||
|               >{$t('done')}</Button |               >{$t('done')}</Button | ||||||
|             > |             > | ||||||
|           {/snippet} |           {/snippet} | ||||||
| @ -579,7 +582,7 @@ | |||||||
|           <AssetGrid |           <AssetGrid | ||||||
|             enableRouting={false} |             enableRouting={false} | ||||||
|             assetStore={timelineStore} |             assetStore={timelineStore} | ||||||
|             assetInteractionStore={timelineInteractionStore} |             assetInteraction={timelineInteraction} | ||||||
|             isSelectionMode={true} |             isSelectionMode={true} | ||||||
|           /> |           /> | ||||||
|         {:else} |         {:else} | ||||||
| @ -587,7 +590,7 @@ | |||||||
|             enableRouting={true} |             enableRouting={true} | ||||||
|             {album} |             {album} | ||||||
|             {assetStore} |             {assetStore} | ||||||
|             {assetInteractionStore} |             {assetInteraction} | ||||||
|             isShared={album.albumUsers.length > 0} |             isShared={album.albumUsers.length > 0} | ||||||
|             isSelectionMode={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL} |             isSelectionMode={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL} | ||||||
|             singleSelect={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL} |             singleSelect={viewMode === AlbumPageViewMode.SELECT_THUMBNAIL} | ||||||
|  | |||||||
| @ -12,12 +12,12 @@ | |||||||
|   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 { AssetAction } from '$lib/constants'; |   import { AssetAction } from '$lib/constants'; | ||||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import { AssetStore } from '$lib/stores/assets.store'; |   import { AssetStore } from '$lib/stores/assets.store'; | ||||||
|   import type { PageData } from './$types'; |   import type { PageData } from './$types'; | ||||||
|   import { mdiPlus, mdiDotsVertical } from '@mdi/js'; |   import { mdiPlus, mdiDotsVertical } from '@mdi/js'; | ||||||
|   import { t } from 'svelte-i18n'; |   import { t } from 'svelte-i18n'; | ||||||
|   import { onDestroy } from 'svelte'; |   import { onDestroy } from 'svelte'; | ||||||
|  |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     data: PageData; |     data: PageData; | ||||||
| @ -26,26 +26,26 @@ | |||||||
|   let { data }: Props = $props(); |   let { data }: Props = $props(); | ||||||
| 
 | 
 | ||||||
|   const assetStore = new AssetStore({ isArchived: true }); |   const assetStore = new AssetStore({ isArchived: true }); | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
|   const { isMultiSelectState, selectedAssets } = assetInteractionStore; |  | ||||||
| 
 |  | ||||||
|   let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); |  | ||||||
| 
 | 
 | ||||||
|   onDestroy(() => { |   onDestroy(() => { | ||||||
|     assetStore.destroy(); |     assetStore.destroy(); | ||||||
|   }); |   }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if $isMultiSelectState} | {#if assetInteraction.selectionActive} | ||||||
|   <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> |   <AssetSelectControlBar | ||||||
|  |     assets={assetInteraction.selectedAssets} | ||||||
|  |     clearSelect={() => assetInteraction.clearMultiselect()} | ||||||
|  |   > | ||||||
|     <ArchiveAction unarchive onArchive={(assetIds) => assetStore.removeAssets(assetIds)} /> |     <ArchiveAction unarchive onArchive={(assetIds) => assetStore.removeAssets(assetIds)} /> | ||||||
|     <CreateSharedLink /> |     <CreateSharedLink /> | ||||||
|     <SelectAllAssets {assetStore} {assetInteractionStore} /> |     <SelectAllAssets {assetStore} {assetInteraction} /> | ||||||
|     <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> |     <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> | ||||||
|       <AddToAlbum /> |       <AddToAlbum /> | ||||||
|       <AddToAlbum shared /> |       <AddToAlbum shared /> | ||||||
|     </ButtonContextMenu> |     </ButtonContextMenu> | ||||||
|     <FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> |     <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> | ||||||
|     <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}> |     <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}> | ||||||
|       <DownloadAction menuItem /> |       <DownloadAction menuItem /> | ||||||
|       <DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} /> |       <DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} /> | ||||||
| @ -53,8 +53,8 @@ | |||||||
|   </AssetSelectControlBar> |   </AssetSelectControlBar> | ||||||
| {/if} | {/if} | ||||||
| 
 | 
 | ||||||
| <UserPageLayout hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}> | <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> | ||||||
|   <AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE}> |   <AssetGrid enableRouting={true} {assetStore} {assetInteraction} removeAction={AssetAction.UNARCHIVE}> | ||||||
|     {#snippet empty()} |     {#snippet empty()} | ||||||
|       <EmptyPlaceholder text={$t('no_archived_assets_message')} /> |       <EmptyPlaceholder text={$t('no_archived_assets_message')} /> | ||||||
|     {/snippet} |     {/snippet} | ||||||
|  | |||||||
| @ -14,7 +14,6 @@ | |||||||
|   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 { AssetAction } from '$lib/constants'; |   import { AssetAction } from '$lib/constants'; | ||||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import { AssetStore } from '$lib/stores/assets.store'; |   import { AssetStore } from '$lib/stores/assets.store'; | ||||||
|   import type { PageData } from './$types'; |   import type { PageData } from './$types'; | ||||||
|   import { mdiDotsVertical, mdiPlus } from '@mdi/js'; |   import { mdiDotsVertical, mdiPlus } from '@mdi/js'; | ||||||
| @ -22,6 +21,7 @@ | |||||||
|   import { onDestroy } from 'svelte'; |   import { onDestroy } from 'svelte'; | ||||||
|   import { preferences } from '$lib/stores/user.store'; |   import { preferences } from '$lib/stores/user.store'; | ||||||
|   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; |   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; | ||||||
|  |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     data: PageData; |     data: PageData; | ||||||
| @ -30,10 +30,7 @@ | |||||||
|   let { data }: Props = $props(); |   let { data }: Props = $props(); | ||||||
| 
 | 
 | ||||||
|   const assetStore = new AssetStore({ isFavorite: true }); |   const assetStore = new AssetStore({ isFavorite: true }); | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
|   const { isMultiSelectState, selectedAssets } = assetInteractionStore; |  | ||||||
| 
 |  | ||||||
|   let isAllArchive = $derived([...$selectedAssets].every((asset) => asset.isArchived)); |  | ||||||
| 
 | 
 | ||||||
|   onDestroy(() => { |   onDestroy(() => { | ||||||
|     assetStore.destroy(); |     assetStore.destroy(); | ||||||
| @ -41,11 +38,14 @@ | |||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <!-- Multiselection mode app bar --> | <!-- Multiselection mode app bar --> | ||||||
| {#if $isMultiSelectState} | {#if assetInteraction.selectionActive} | ||||||
|   <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> |   <AssetSelectControlBar | ||||||
|  |     assets={assetInteraction.selectedAssets} | ||||||
|  |     clearSelect={() => assetInteraction.clearMultiselect()} | ||||||
|  |   > | ||||||
|     <FavoriteAction removeFavorite onFavorite={(assetIds) => assetStore.removeAssets(assetIds)} /> |     <FavoriteAction removeFavorite onFavorite={(assetIds) => assetStore.removeAssets(assetIds)} /> | ||||||
|     <CreateSharedLink /> |     <CreateSharedLink /> | ||||||
|     <SelectAllAssets {assetStore} {assetInteractionStore} /> |     <SelectAllAssets {assetStore} {assetInteraction} /> | ||||||
|     <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> |     <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> | ||||||
|       <AddToAlbum /> |       <AddToAlbum /> | ||||||
|       <AddToAlbum shared /> |       <AddToAlbum shared /> | ||||||
| @ -54,7 +54,11 @@ | |||||||
|       <DownloadAction menuItem /> |       <DownloadAction menuItem /> | ||||||
|       <ChangeDate menuItem /> |       <ChangeDate menuItem /> | ||||||
|       <ChangeLocation menuItem /> |       <ChangeLocation menuItem /> | ||||||
|       <ArchiveAction menuItem unarchive={isAllArchive} onArchive={(assetIds) => assetStore.removeAssets(assetIds)} /> |       <ArchiveAction | ||||||
|  |         menuItem | ||||||
|  |         unarchive={assetInteraction.isAllArchived} | ||||||
|  |         onArchive={(assetIds) => assetStore.removeAssets(assetIds)} | ||||||
|  |       /> | ||||||
|       {#if $preferences.tags.enabled} |       {#if $preferences.tags.enabled} | ||||||
|         <TagAction menuItem /> |         <TagAction menuItem /> | ||||||
|       {/if} |       {/if} | ||||||
| @ -63,8 +67,8 @@ | |||||||
|   </AssetSelectControlBar> |   </AssetSelectControlBar> | ||||||
| {/if} | {/if} | ||||||
| 
 | 
 | ||||||
| <UserPageLayout hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}> | <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> | ||||||
|   <AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} removeAction={AssetAction.UNFAVORITE}> |   <AssetGrid enableRouting={true} {assetStore} {assetInteraction} removeAction={AssetAction.UNFAVORITE}> | ||||||
|     {#snippet empty()} |     {#snippet empty()} | ||||||
|       <EmptyPlaceholder text={$t('no_favorites_message')} /> |       <EmptyPlaceholder text={$t('no_favorites_message')} /> | ||||||
|     {/snippet} |     {/snippet} | ||||||
|  | |||||||
| @ -3,7 +3,6 @@ | |||||||
|   import { page } from '$app/stores'; |   import { page } from '$app/stores'; | ||||||
|   import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte'; |   import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte'; | ||||||
|   import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; |   import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; | ||||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte'; |   import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte'; | ||||||
|   import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; |   import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; | ||||||
|   import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; |   import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; | ||||||
| @ -17,6 +16,7 @@ | |||||||
|   import type { PageData } from './$types'; |   import type { PageData } from './$types'; | ||||||
|   import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; |   import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; | ||||||
|   import SkipLink from '$lib/components/elements/buttons/skip-link.svelte'; |   import SkipLink from '$lib/components/elements/buttons/skip-link.svelte'; | ||||||
|  |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     data: PageData; |     data: PageData; | ||||||
| @ -31,7 +31,7 @@ | |||||||
|   let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || ''); |   let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || ''); | ||||||
|   let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree)); |   let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree)); | ||||||
| 
 | 
 | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
| 
 | 
 | ||||||
|   onMount(async () => { |   onMount(async () => { | ||||||
|     await foldersStore.fetchUniquePaths(); |     await foldersStore.fetchUniquePaths(); | ||||||
| @ -80,7 +80,7 @@ | |||||||
|       <div bind:clientHeight={viewport.height} bind:clientWidth={viewport.width} class="mt-2"> |       <div bind:clientHeight={viewport.height} bind:clientWidth={viewport.width} class="mt-2"> | ||||||
|         <GalleryViewer |         <GalleryViewer | ||||||
|           assets={data.pathAssets} |           assets={data.pathAssets} | ||||||
|           {assetInteractionStore} |           {assetInteraction} | ||||||
|           {viewport} |           {viewport} | ||||||
|           disableAssetSelect={true} |           disableAssetSelect={true} | ||||||
|           showAssetName={true} |           showAssetName={true} | ||||||
|  | |||||||
| @ -8,12 +8,12 @@ | |||||||
|   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 { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import { AssetStore } from '$lib/stores/assets.store'; |   import { AssetStore } from '$lib/stores/assets.store'; | ||||||
|   import { onDestroy } from 'svelte'; |   import { onDestroy } from 'svelte'; | ||||||
|   import type { PageData } from './$types'; |   import type { PageData } from './$types'; | ||||||
|   import { mdiPlus, mdiArrowLeft } from '@mdi/js'; |   import { mdiPlus, mdiArrowLeft } from '@mdi/js'; | ||||||
|   import { t } from 'svelte-i18n'; |   import { t } from 'svelte-i18n'; | ||||||
|  |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     data: PageData; |     data: PageData; | ||||||
| @ -22,18 +22,16 @@ | |||||||
|   let { data }: Props = $props(); |   let { data }: Props = $props(); | ||||||
| 
 | 
 | ||||||
|   const assetStore = new AssetStore({ userId: data.partner.id, isArchived: false, withStacked: true }); |   const assetStore = new AssetStore({ userId: data.partner.id, isArchived: false, withStacked: true }); | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
|   const { isMultiSelectState, selectedAssets, clearMultiselect } = assetInteractionStore; |  | ||||||
| 
 | 
 | ||||||
|   onDestroy(() => { |   onDestroy(() => { | ||||||
|     assetInteractionStore.clearMultiselect(); |  | ||||||
|     assetStore.destroy(); |     assetStore.destroy(); | ||||||
|   }); |   }); | ||||||
| </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 $isMultiSelectState} |   {#if assetInteraction.selectionActive} | ||||||
|     <AssetSelectControlBar assets={$selectedAssets} clearSelect={clearMultiselect}> |     <AssetSelectControlBar assets={assetInteraction.selectedAssets} clearSelect={assetInteraction.clearMultiselect}> | ||||||
|       <CreateSharedLink /> |       <CreateSharedLink /> | ||||||
|       <ButtonContextMenu icon={mdiPlus} title={$t('add')}> |       <ButtonContextMenu icon={mdiPlus} title={$t('add')}> | ||||||
|         <AddToAlbum /> |         <AddToAlbum /> | ||||||
| @ -50,5 +48,5 @@ | |||||||
|       {/snippet} |       {/snippet} | ||||||
|     </ControlAppBar> |     </ControlAppBar> | ||||||
|   {/if} |   {/if} | ||||||
|   <AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} /> |   <AssetGrid enableRouting={true} {assetStore} {assetInteraction} /> | ||||||
| </main> | </main> | ||||||
|  | |||||||
| @ -27,7 +27,6 @@ | |||||||
|     notificationController, |     notificationController, | ||||||
|   } from '$lib/components/shared-components/notification/notification'; |   } from '$lib/components/shared-components/notification/notification'; | ||||||
|   import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants'; |   import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants'; | ||||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; |   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||||
|   import { AssetStore } from '$lib/stores/assets.store'; |   import { AssetStore } from '$lib/stores/assets.store'; | ||||||
|   import { websocketEvents } from '$lib/stores/websocket'; |   import { websocketEvents } from '$lib/stores/websocket'; | ||||||
| @ -58,8 +57,9 @@ | |||||||
|   import { listNavigation } from '$lib/actions/list-navigation'; |   import { listNavigation } from '$lib/actions/list-navigation'; | ||||||
|   import { t } from 'svelte-i18n'; |   import { t } from 'svelte-i18n'; | ||||||
|   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; |   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; | ||||||
|   import { preferences, user } from '$lib/stores/user.store'; |   import { preferences } from '$lib/stores/user.store'; | ||||||
|   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; |   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; | ||||||
|  |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     data: PageData; |     data: PageData; | ||||||
| @ -78,8 +78,7 @@ | |||||||
|     handlePromiseError(assetStore.updateOptions(assetStoreOptions)); |     handlePromiseError(assetStore.updateOptions(assetStoreOptions)); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
|   const { selectedAssets, isMultiSelectState } = assetInteractionStore; |  | ||||||
| 
 | 
 | ||||||
|   let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS); |   let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS); | ||||||
|   let isEditingName = $state(false); |   let isEditingName = $state(false); | ||||||
| @ -123,8 +122,8 @@ | |||||||
|     if ($showAssetViewer || viewMode === PersonPageViewMode.SUGGEST_MERGE) { |     if ($showAssetViewer || viewMode === PersonPageViewMode.SUGGEST_MERGE) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     if ($isMultiSelectState) { |     if (assetInteraction.selectionActive) { | ||||||
|       assetInteractionStore.clearMultiselect(); |       assetInteraction.clearMultiselect(); | ||||||
|       return; |       return; | ||||||
|     } else { |     } else { | ||||||
|       await goto(previousRoute); |       await goto(previousRoute); | ||||||
| @ -149,8 +148,8 @@ | |||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   const handleUnmerge = () => { |   const handleUnmerge = () => { | ||||||
|     $assetStore.removeAssets([...$selectedAssets].map((a) => a.id)); |     $assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id)); | ||||||
|     assetInteractionStore.clearMultiselect(); |     assetInteraction.clearMultiselect(); | ||||||
|     viewMode = PersonPageViewMode.VIEW_ASSETS; |     viewMode = PersonPageViewMode.VIEW_ASSETS; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
| @ -194,7 +193,7 @@ | |||||||
|       handleError(error, $t('errors.unable_to_set_feature_photo')); |       handleError(error, $t('errors.unable_to_set_feature_photo')); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     assetInteractionStore.clearMultiselect(); |     assetInteraction.clearMultiselect(); | ||||||
| 
 | 
 | ||||||
|     viewMode = PersonPageViewMode.VIEW_ASSETS; |     viewMode = PersonPageViewMode.VIEW_ASSETS; | ||||||
|   }; |   }; | ||||||
| @ -336,15 +335,11 @@ | |||||||
|       handlePromiseError(updateAssetCount()); |       handlePromiseError(updateAssetCount()); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| 
 |  | ||||||
|   let isAllArchive = $derived([...$selectedAssets].every((asset) => asset.isArchived)); |  | ||||||
|   let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); |  | ||||||
|   let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS} | {#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS} | ||||||
|   <UnMergeFaceSelector |   <UnMergeFaceSelector | ||||||
|     assetIds={[...$selectedAssets].map((a) => a.id)} |     assetIds={assetInteraction.selectedAssetsArray.map((a) => a.id)} | ||||||
|     personAssets={person} |     personAssets={person} | ||||||
|     onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)} |     onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)} | ||||||
|     onConfirm={handleUnmerge} |     onConfirm={handleUnmerge} | ||||||
| @ -375,15 +370,18 @@ | |||||||
| {/if} | {/if} | ||||||
| 
 | 
 | ||||||
| <header> | <header> | ||||||
|   {#if $isMultiSelectState} |   {#if assetInteraction.selectionActive} | ||||||
|     <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> |     <AssetSelectControlBar | ||||||
|  |       assets={assetInteraction.selectedAssets} | ||||||
|  |       clearSelect={() => assetInteraction.clearMultiselect()} | ||||||
|  |     > | ||||||
|       <CreateSharedLink /> |       <CreateSharedLink /> | ||||||
|       <SelectAllAssets {assetStore} {assetInteractionStore} /> |       <SelectAllAssets {assetStore} {assetInteraction} /> | ||||||
|       <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> |       <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> | ||||||
|         <AddToAlbum /> |         <AddToAlbum /> | ||||||
|         <AddToAlbum shared /> |         <AddToAlbum shared /> | ||||||
|       </ButtonContextMenu> |       </ButtonContextMenu> | ||||||
|       <FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> |       <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> | ||||||
|       <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}> |       <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}> | ||||||
|         <DownloadAction menuItem filename="{person.name || 'immich'}.zip" /> |         <DownloadAction menuItem filename="{person.name || 'immich'}.zip" /> | ||||||
|         <MenuOption |         <MenuOption | ||||||
| @ -393,8 +391,12 @@ | |||||||
|         /> |         /> | ||||||
|         <ChangeDate menuItem /> |         <ChangeDate menuItem /> | ||||||
|         <ChangeLocation menuItem /> |         <ChangeLocation menuItem /> | ||||||
|         <ArchiveAction menuItem unarchive={isAllArchive} onArchive={(assetIds) => $assetStore.removeAssets(assetIds)} /> |         <ArchiveAction | ||||||
|         {#if $preferences.tags.enabled && isAllUserOwned} |           menuItem | ||||||
|  |           unarchive={assetInteraction.isAllArchived} | ||||||
|  |           onArchive={(assetIds) => $assetStore.removeAssets(assetIds)} | ||||||
|  |         /> | ||||||
|  |         {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} | ||||||
|           <TagAction menuItem /> |           <TagAction menuItem /> | ||||||
|         {/if} |         {/if} | ||||||
|         <DeleteAssets menuItem onAssetDelete={(assetIds) => $assetStore.removeAssets(assetIds)} /> |         <DeleteAssets menuItem onAssetDelete={(assetIds) => $assetStore.removeAssets(assetIds)} /> | ||||||
| @ -453,7 +455,7 @@ | |||||||
|     <AssetGrid |     <AssetGrid | ||||||
|       enableRouting={true} |       enableRouting={true} | ||||||
|       {assetStore} |       {assetStore} | ||||||
|       {assetInteractionStore} |       {assetInteraction} | ||||||
|       isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON} |       isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON} | ||||||
|       singleSelect={viewMode === PersonPageViewMode.SELECT_PERSON} |       singleSelect={viewMode === PersonPageViewMode.SELECT_PERSON} | ||||||
|       onSelect={handleSelectFeaturePhoto} |       onSelect={handleSelectFeaturePhoto} | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ | |||||||
|   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; |   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; | ||||||
|   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; |   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; | ||||||
|   import { AssetAction } from '$lib/constants'; |   import { AssetAction } from '$lib/constants'; | ||||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; |   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||||
|   import { AssetStore } from '$lib/stores/assets.store'; |   import { AssetStore } from '$lib/stores/assets.store'; | ||||||
|   import { preferences, user } from '$lib/stores/user.store'; |   import { preferences, user } from '$lib/stores/user.store'; | ||||||
| @ -32,33 +32,25 @@ | |||||||
| 
 | 
 | ||||||
|   let { isViewing: showAssetViewer } = assetViewingStore; |   let { isViewing: showAssetViewer } = assetViewingStore; | ||||||
|   const assetStore = new AssetStore({ isArchived: false, withStacked: true, withPartners: true }); |   const assetStore = new AssetStore({ isArchived: false, withStacked: true, withPartners: true }); | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
|   const { isMultiSelectState, selectedAssets } = assetInteractionStore; |  | ||||||
| 
 | 
 | ||||||
|   let isAllFavorite = $state(false); |   let selectedAssets = $derived(assetInteraction.selectedAssetsArray); | ||||||
|   let isAllOwned = $state(false); |   let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack); | ||||||
|   let isAssetStackSelected = $state(false); |   let isLinkActionAvailable = $derived.by(() => { | ||||||
|   let isLinkActionAvailable = $state(false); |     const isLivePhoto = selectedAssets.length === 1 && !!selectedAssets[0].livePhotoVideoId; | ||||||
| 
 |  | ||||||
|   $effect(() => { |  | ||||||
|     const selection = [...$selectedAssets]; |  | ||||||
|     isAllOwned = selection.every((asset) => asset.ownerId === $user.id); |  | ||||||
|     isAllFavorite = selection.every((asset) => asset.isFavorite); |  | ||||||
|     isAssetStackSelected = selection.length === 1 && !!selection[0].stack; |  | ||||||
|     const isLivePhoto = selection.length === 1 && !!selection[0].livePhotoVideoId; |  | ||||||
|     const isLivePhotoCandidate = |     const isLivePhotoCandidate = | ||||||
|       selection.length === 2 && |       selectedAssets.length === 2 && | ||||||
|       selection.some((asset) => asset.type === AssetTypeEnum.Image) && |       selectedAssets.some((asset) => asset.type === AssetTypeEnum.Image) && | ||||||
|       selection.some((asset) => asset.type === AssetTypeEnum.Video); |       selectedAssets.some((asset) => asset.type === AssetTypeEnum.Video); | ||||||
|     isLinkActionAvailable = isAllOwned && (isLivePhoto || isLivePhotoCandidate); |  | ||||||
|   }); |  | ||||||
| 
 | 
 | ||||||
|  |     return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate); | ||||||
|  |   }); | ||||||
|   const handleEscape = () => { |   const handleEscape = () => { | ||||||
|     if ($showAssetViewer) { |     if ($showAssetViewer) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     if ($isMultiSelectState) { |     if (assetInteraction.selectionActive) { | ||||||
|       assetInteractionStore.clearMultiselect(); |       assetInteraction.clearMultiselect(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| @ -78,22 +70,22 @@ | |||||||
|   }); |   }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if $isMultiSelectState} | {#if assetInteraction.selectionActive} | ||||||
|   <AssetSelectControlBar |   <AssetSelectControlBar | ||||||
|     ownerId={$user.id} |     ownerId={$user.id} | ||||||
|     assets={$selectedAssets} |     assets={assetInteraction.selectedAssets} | ||||||
|     clearSelect={() => assetInteractionStore.clearMultiselect()} |     clearSelect={() => assetInteraction.clearMultiselect()} | ||||||
|   > |   > | ||||||
|     <CreateSharedLink /> |     <CreateSharedLink /> | ||||||
|     <SelectAllAssets {assetStore} {assetInteractionStore} /> |     <SelectAllAssets {assetStore} {assetInteraction} /> | ||||||
|     <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> |     <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> | ||||||
|       <AddToAlbum /> |       <AddToAlbum /> | ||||||
|       <AddToAlbum shared /> |       <AddToAlbum shared /> | ||||||
|     </ButtonContextMenu> |     </ButtonContextMenu> | ||||||
|     <FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> |     <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> | ||||||
|     <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> |     <ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}> | ||||||
|       <DownloadAction menuItem /> |       <DownloadAction menuItem /> | ||||||
|       {#if $selectedAssets.size > 1 || isAssetStackSelected} |       {#if assetInteraction.selectedAssets.size > 1 || isAssetStackSelected} | ||||||
|         <StackAction |         <StackAction | ||||||
|           unstack={isAssetStackSelected} |           unstack={isAssetStackSelected} | ||||||
|           onStack={(assetIds) => assetStore.removeAssets(assetIds)} |           onStack={(assetIds) => assetStore.removeAssets(assetIds)} | ||||||
| @ -103,7 +95,7 @@ | |||||||
|       {#if isLinkActionAvailable} |       {#if isLinkActionAvailable} | ||||||
|         <LinkLivePhotoAction |         <LinkLivePhotoAction | ||||||
|           menuItem |           menuItem | ||||||
|           unlink={[...$selectedAssets].length === 1} |           unlink={assetInteraction.selectedAssets.size === 1} | ||||||
|           onLink={handleLink} |           onLink={handleLink} | ||||||
|           onUnlink={handleUnlink} |           onUnlink={handleUnlink} | ||||||
|         /> |         /> | ||||||
| @ -121,11 +113,11 @@ | |||||||
|   </AssetSelectControlBar> |   </AssetSelectControlBar> | ||||||
| {/if} | {/if} | ||||||
| 
 | 
 | ||||||
| <UserPageLayout hideNavbar={$isMultiSelectState} showUploadButton scrollbar={false}> | <UserPageLayout hideNavbar={assetInteraction.selectionActive} showUploadButton scrollbar={false}> | ||||||
|   <AssetGrid |   <AssetGrid | ||||||
|     enableRouting={true} |     enableRouting={true} | ||||||
|     {assetStore} |     {assetStore} | ||||||
|     {assetInteractionStore} |     {assetInteraction} | ||||||
|     removeAction={AssetAction.ARCHIVE} |     removeAction={AssetAction.ARCHIVE} | ||||||
|     onEscape={handleEscape} |     onEscape={handleEscape} | ||||||
|     withStacked |     withStacked | ||||||
|  | |||||||
| @ -16,7 +16,6 @@ | |||||||
|   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; |   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; | ||||||
|   import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; |   import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; | ||||||
|   import { cancelMultiselect } from '$lib/utils/asset-utils'; |   import { cancelMultiselect } from '$lib/utils/asset-utils'; | ||||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; |   import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; | ||||||
|   import { AppRoute, QueryParameter } from '$lib/constants'; |   import { AppRoute, QueryParameter } from '$lib/constants'; | ||||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; |   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||||
| @ -44,8 +43,9 @@ | |||||||
|   import { t } from 'svelte-i18n'; |   import { t } from 'svelte-i18n'; | ||||||
|   import { onMount, tick } from 'svelte'; |   import { onMount, tick } from 'svelte'; | ||||||
|   import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte'; |   import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte'; | ||||||
|   import { preferences, user } from '$lib/stores/user.store'; |   import { preferences } from '$lib/stores/user.store'; | ||||||
|   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; |   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; | ||||||
|  |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   const MAX_ASSET_COUNT = 5000; |   const MAX_ASSET_COUNT = 5000; | ||||||
|   let { isViewing: showAssetViewer } = assetViewingStore; |   let { isViewing: showAssetViewer } = assetViewingStore; | ||||||
| @ -63,14 +63,9 @@ | |||||||
|   let scrollY = $state(0); |   let scrollY = $state(0); | ||||||
|   let scrollYHistory = 0; |   let scrollYHistory = 0; | ||||||
| 
 | 
 | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
|   const { selectedAssets } = assetInteractionStore; |  | ||||||
| 
 | 
 | ||||||
|   type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>; |   type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>; | ||||||
| 
 |  | ||||||
|   let isMultiSelectionMode = $derived($selectedAssets.size > 0); |  | ||||||
|   let isAllArchived = $derived([...$selectedAssets].every((asset) => asset.isArchived)); |  | ||||||
|   let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); |  | ||||||
|   let searchQuery = $derived($page.url.searchParams.get(QueryParameter.QUERY)); |   let searchQuery = $derived($page.url.searchParams.get(QueryParameter.QUERY)); | ||||||
| 
 | 
 | ||||||
|   onMount(() => { |   onMount(() => { | ||||||
| @ -86,8 +81,8 @@ | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (isMultiSelectionMode) { |     if (assetInteraction.selectionActive) { | ||||||
|       $selectedAssets = new Set(); |       assetInteraction.selectedAssets.clear(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     if (!$preventRaceConditionSearchBar) { |     if (!$preventRaceConditionSearchBar) { | ||||||
| @ -131,7 +126,7 @@ | |||||||
|     searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id)); |     searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id)); | ||||||
|   }; |   }; | ||||||
|   const handleSelectAll = () => { |   const handleSelectAll = () => { | ||||||
|     assetInteractionStore.selectAssets(searchResultAssets); |     assetInteraction.selectAssets(searchResultAssets); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   async function onSearchQueryUpdate() { |   async function onSearchQueryUpdate() { | ||||||
| @ -231,29 +226,31 @@ | |||||||
|   function getObjectKeys<T extends object>(obj: T): (keyof T)[] { |   function getObjectKeys<T extends object>(obj: T): (keyof T)[] { | ||||||
|     return Object.keys(obj) as (keyof T)[]; |     return Object.keys(obj) as (keyof T)[]; | ||||||
|   } |   } | ||||||
|   let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <svelte:window use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onEscape }} bind:scrollY /> | <svelte:window use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onEscape }} bind:scrollY /> | ||||||
| 
 | 
 | ||||||
| <section> | <section> | ||||||
|   {#if isMultiSelectionMode} |   {#if assetInteraction.selectionActive} | ||||||
|     <div class="fixed z-[100] top-0 left-0 w-full"> |     <div class="fixed z-[100] top-0 left-0 w-full"> | ||||||
|       <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => cancelMultiselect(assetInteractionStore)}> |       <AssetSelectControlBar | ||||||
|  |         assets={assetInteraction.selectedAssets} | ||||||
|  |         clearSelect={() => cancelMultiselect(assetInteraction)} | ||||||
|  |       > | ||||||
|         <CreateSharedLink /> |         <CreateSharedLink /> | ||||||
|         <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} /> |         <CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} /> | ||||||
|         <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> |         <ButtonContextMenu icon={mdiPlus} title={$t('add_to')}> | ||||||
|           <AddToAlbum {onAddToAlbum} /> |           <AddToAlbum {onAddToAlbum} /> | ||||||
|           <AddToAlbum shared {onAddToAlbum} /> |           <AddToAlbum shared {onAddToAlbum} /> | ||||||
|         </ButtonContextMenu> |         </ButtonContextMenu> | ||||||
|         <FavoriteAction removeFavorite={isAllFavorite} onFavorite={triggerAssetUpdate} /> |         <FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={triggerAssetUpdate} /> | ||||||
| 
 | 
 | ||||||
|         <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}> |         <ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}> | ||||||
|           <DownloadAction menuItem /> |           <DownloadAction menuItem /> | ||||||
|           <ChangeDate menuItem /> |           <ChangeDate menuItem /> | ||||||
|           <ChangeLocation menuItem /> |           <ChangeLocation menuItem /> | ||||||
|           <ArchiveAction menuItem unarchive={isAllArchived} onArchive={triggerAssetUpdate} /> |           <ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={triggerAssetUpdate} /> | ||||||
|           {#if $preferences.tags.enabled && isAllUserOwned} |           {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} | ||||||
|             <TagAction menuItem /> |             <TagAction menuItem /> | ||||||
|           {/if} |           {/if} | ||||||
|           <DeleteAssets menuItem {onAssetDelete} /> |           <DeleteAssets menuItem {onAssetDelete} /> | ||||||
| @ -333,7 +330,7 @@ | |||||||
|       {#if searchResultAssets.length > 0} |       {#if searchResultAssets.length > 0} | ||||||
|         <GalleryViewer |         <GalleryViewer | ||||||
|           assets={searchResultAssets} |           assets={searchResultAssets} | ||||||
|           {assetInteractionStore} |           {assetInteraction} | ||||||
|           onIntersected={loadNextPage} |           onIntersected={loadNextPage} | ||||||
|           showArchiveIcon={true} |           showArchiveIcon={true} | ||||||
|           {viewport} |           {viewport} | ||||||
|  | |||||||
| @ -16,7 +16,6 @@ | |||||||
|   import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; |   import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; | ||||||
|   import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; |   import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; | ||||||
|   import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants'; |   import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants'; | ||||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import { AssetStore } from '$lib/stores/assets.store'; |   import { AssetStore } from '$lib/stores/assets.store'; | ||||||
|   import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; |   import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; | ||||||
|   import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk'; |   import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk'; | ||||||
| @ -26,6 +25,7 @@ | |||||||
|   import { dialogController } from '$lib/components/shared-components/dialog/dialog'; |   import { dialogController } from '$lib/components/shared-components/dialog/dialog'; | ||||||
|   import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; |   import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; | ||||||
|   import SkipLink from '$lib/components/elements/buttons/skip-link.svelte'; |   import SkipLink from '$lib/components/elements/buttons/skip-link.svelte'; | ||||||
|  |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     data: PageData; |     data: PageData; | ||||||
| @ -36,7 +36,7 @@ | |||||||
|   let pathSegments = $derived(data.path ? data.path.split('/') : []); |   let pathSegments = $derived(data.path ? data.path.split('/') : []); | ||||||
|   let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || ''); |   let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || ''); | ||||||
| 
 | 
 | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
| 
 | 
 | ||||||
|   const buildMap = (tags: TagResponseDto[]) => { |   const buildMap = (tags: TagResponseDto[]) => { | ||||||
|     return Object.fromEntries(tags.map((tag) => [tag.value, tag])); |     return Object.fromEntries(tags.map((tag) => [tag.value, tag])); | ||||||
| @ -198,7 +198,7 @@ | |||||||
| 
 | 
 | ||||||
|   <section class="mt-2 h-full"> |   <section class="mt-2 h-full"> | ||||||
|     {#if tag} |     {#if tag} | ||||||
|       <AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE}> |       <AssetGrid enableRouting={true} {assetStore} {assetInteraction} removeAction={AssetAction.UNARCHIVE}> | ||||||
|         {#snippet empty()} |         {#snippet empty()} | ||||||
|           <TreeItemThumbnails items={data.children} icon={mdiTag} onClick={handleNavigation} /> |           <TreeItemThumbnails items={data.children} icon={mdiTag} onClick={handleNavigation} /> | ||||||
|         {/snippet} |         {/snippet} | ||||||
|  | |||||||
| @ -15,7 +15,6 @@ | |||||||
|     notificationController, |     notificationController, | ||||||
|   } from '$lib/components/shared-components/notification/notification'; |   } from '$lib/components/shared-components/notification/notification'; | ||||||
|   import { AppRoute } from '$lib/constants'; |   import { AppRoute } from '$lib/constants'; | ||||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; |  | ||||||
|   import { AssetStore } from '$lib/stores/assets.store'; |   import { AssetStore } from '$lib/stores/assets.store'; | ||||||
|   import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; |   import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; | ||||||
|   import { handleError } from '$lib/utils/handle-error'; |   import { handleError } from '$lib/utils/handle-error'; | ||||||
| @ -26,6 +25,7 @@ | |||||||
|   import { dialogController } from '$lib/components/shared-components/dialog/dialog'; |   import { dialogController } from '$lib/components/shared-components/dialog/dialog'; | ||||||
|   import { t } from 'svelte-i18n'; |   import { t } from 'svelte-i18n'; | ||||||
|   import { onDestroy } from 'svelte'; |   import { onDestroy } from 'svelte'; | ||||||
|  |   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||||
| 
 | 
 | ||||||
|   interface Props { |   interface Props { | ||||||
|     data: PageData; |     data: PageData; | ||||||
| @ -39,8 +39,7 @@ | |||||||
| 
 | 
 | ||||||
|   const options = { isTrashed: true }; |   const options = { isTrashed: true }; | ||||||
|   const assetStore = new AssetStore(options); |   const assetStore = new AssetStore(options); | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteraction = new AssetInteraction(); | ||||||
|   const { isMultiSelectState, selectedAssets } = assetInteractionStore; |  | ||||||
| 
 | 
 | ||||||
|   const handleEmptyTrash = async () => { |   const handleEmptyTrash = async () => { | ||||||
|     const isConfirmed = await dialogController.show({ |     const isConfirmed = await dialogController.show({ | ||||||
| @ -93,25 +92,28 @@ | |||||||
|   }); |   }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if $isMultiSelectState} | {#if assetInteraction.selectionActive} | ||||||
|   <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> |   <AssetSelectControlBar | ||||||
|     <SelectAllAssets {assetStore} {assetInteractionStore} /> |     assets={assetInteraction.selectedAssets} | ||||||
|  |     clearSelect={() => assetInteraction.clearMultiselect()} | ||||||
|  |   > | ||||||
|  |     <SelectAllAssets {assetStore} {assetInteraction} /> | ||||||
|     <DeleteAssets force onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} /> |     <DeleteAssets force onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} /> | ||||||
|     <RestoreAssets onRestore={(assetIds) => assetStore.removeAssets(assetIds)} /> |     <RestoreAssets onRestore={(assetIds) => assetStore.removeAssets(assetIds)} /> | ||||||
|   </AssetSelectControlBar> |   </AssetSelectControlBar> | ||||||
| {/if} | {/if} | ||||||
| 
 | 
 | ||||||
| {#if $featureFlags.loaded && $featureFlags.trash} | {#if $featureFlags.loaded && $featureFlags.trash} | ||||||
|   <UserPageLayout hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}> |   <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> | ||||||
|     {#snippet buttons()} |     {#snippet buttons()} | ||||||
|       <div class="flex place-items-center gap-2"> |       <div class="flex place-items-center gap-2"> | ||||||
|         <LinkButton onclick={handleRestoreTrash} disabled={$isMultiSelectState}> |         <LinkButton onclick={handleRestoreTrash} disabled={assetInteraction.selectionActive}> | ||||||
|           <div class="flex place-items-center gap-2 text-sm"> |           <div class="flex place-items-center gap-2 text-sm"> | ||||||
|             <Icon path={mdiHistory} size="18" /> |             <Icon path={mdiHistory} size="18" /> | ||||||
|             {$t('restore_all')} |             {$t('restore_all')} | ||||||
|           </div> |           </div> | ||||||
|         </LinkButton> |         </LinkButton> | ||||||
|         <LinkButton onclick={() => handleEmptyTrash()} disabled={$isMultiSelectState}> |         <LinkButton onclick={() => handleEmptyTrash()} disabled={assetInteraction.selectionActive}> | ||||||
|           <div class="flex place-items-center gap-2 text-sm"> |           <div class="flex place-items-center gap-2 text-sm"> | ||||||
|             <Icon path={mdiDeleteForeverOutline} size="18" /> |             <Icon path={mdiDeleteForeverOutline} size="18" /> | ||||||
|             {$t('empty_trash')} |             {$t('empty_trash')} | ||||||
| @ -120,7 +122,7 @@ | |||||||
|       </div> |       </div> | ||||||
|     {/snippet} |     {/snippet} | ||||||
| 
 | 
 | ||||||
|     <AssetGrid enableRouting={true} {assetStore} {assetInteractionStore}> |     <AssetGrid enableRouting={true} {assetStore} {assetInteraction}> | ||||||
|       <p class="font-medium text-gray-500/60 dark:text-gray-300/60 p-4"> |       <p class="font-medium text-gray-500/60 dark:text-gray-300/60 p-4"> | ||||||
|         {$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })} |         {$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })} | ||||||
|       </p> |       </p> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user