mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:37:11 -04:00 
			
		
		
		
	feat(web): un-stack from the photos page ; fix stack count (#8419)
* feat(web): un-stack from the photos page ; fix stack count * move stuff outside of try-catch block * small optim
This commit is contained in:
		
							parent
							
								
									a3feca2580
								
							
						
					
					
						commit
						c227f9893e
					
				| @ -12,7 +12,7 @@ | ||||
|   import { stackAssetsStore } from '$lib/stores/stacked-asset.store'; | ||||
|   import { user } from '$lib/stores/user.store'; | ||||
|   import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils'; | ||||
|   import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile } from '$lib/utils/asset-utils'; | ||||
|   import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile, unstackAssets } from '$lib/utils/asset-utils'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { shortcuts } from '$lib/utils/shortcut'; | ||||
|   import { SlideshowHistory } from '$lib/utils/slideshow-history'; | ||||
| @ -28,7 +28,6 @@ | ||||
|     getAllAlbums, | ||||
|     runAssetJobs, | ||||
|     updateAsset, | ||||
|     updateAssets, | ||||
|     updateAlbumInfo, | ||||
|     type ActivityResponseDto, | ||||
|     type AlbumResponseDto, | ||||
| @ -481,20 +480,15 @@ | ||||
|   }; | ||||
| 
 | ||||
|   const handleUnstack = async () => { | ||||
|     try { | ||||
|       const ids = $stackAssetsStore.map(({ id }) => id); | ||||
|       await updateAssets({ assetBulkUpdateDto: { ids, removeParent: true } }); | ||||
|       for (const child of $stackAssetsStore) { | ||||
|         child.stackParentId = null; | ||||
|         child.stackCount = 0; | ||||
|         child.stack = []; | ||||
|         dispatch('action', { type: AssetAction.ADD, asset: child }); | ||||
|     const unstackedAssets = await unstackAssets($stackAssetsStore); | ||||
|     if (unstackedAssets) { | ||||
|       for (const asset of unstackedAssets) { | ||||
|         dispatch('action', { | ||||
|           type: AssetAction.ADD, | ||||
|           asset, | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       dispatch('close'); | ||||
|       notificationController.show({ type: NotificationType.Info, message: 'Un-stacked', timeout: 1500 }); | ||||
|     } catch (error) { | ||||
|       handleError(error, `Unable to unstack`); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|  | ||||
| @ -1,20 +1,45 @@ | ||||
| <script lang="ts"> | ||||
|   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; | ||||
|   import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||
|   import type { OnStack } from '$lib/utils/actions'; | ||||
|   import { stackAssets } from '$lib/utils/asset-utils'; | ||||
|   import { mdiImageMultipleOutline } from '@mdi/js'; | ||||
|   import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte'; | ||||
|   import { mdiImageMinusOutline, mdiImageMultipleOutline } from '@mdi/js'; | ||||
|   import { stackAssets, unstackAssets } from '$lib/utils/asset-utils'; | ||||
|   import type { OnStack, OnUnstack } from '$lib/utils/actions'; | ||||
| 
 | ||||
|   export let unstack = false; | ||||
|   export let onStack: OnStack | undefined; | ||||
|   export let onUnstack: OnUnstack | undefined; | ||||
| 
 | ||||
|   const { clearSelect, getOwnedAssets } = getAssetControlContext(); | ||||
| 
 | ||||
|   const handleStack = async () => { | ||||
|     await stackAssets([...getOwnedAssets()], (ids) => { | ||||
|     const selectedAssets = [...getOwnedAssets()]; | ||||
|     const ids = await stackAssets(selectedAssets); | ||||
|     if (ids) { | ||||
|       onStack?.(ids); | ||||
|       clearSelect(); | ||||
|     }); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleUnstack = async () => { | ||||
|     const selectedAssets = [...getOwnedAssets()]; | ||||
|     if (selectedAssets.length !== 1) { | ||||
|       return; | ||||
|     } | ||||
|     const { stack } = selectedAssets[0]; | ||||
|     if (!stack) { | ||||
|       return; | ||||
|     } | ||||
|     const assets = [selectedAssets[0], ...stack]; | ||||
|     const unstackedAssets = await unstackAssets(assets); | ||||
|     if (unstackedAssets) { | ||||
|       onUnstack?.(unstackedAssets); | ||||
|     } | ||||
|     clearSelect(); | ||||
|   }; | ||||
| </script> | ||||
| 
 | ||||
| <MenuOption text="Stack" icon={mdiImageMultipleOutline} on:click={handleStack} /> | ||||
| {#if unstack} | ||||
|   <MenuOption text="Un-stack" icon={mdiImageMinusOutline} on:click={handleUnstack} /> | ||||
| {:else} | ||||
|   <MenuOption text="Stack" icon={mdiImageMultipleOutline} on:click={handleStack} /> | ||||
| {/if} | ||||
|  | ||||
| @ -89,11 +89,10 @@ | ||||
|   }; | ||||
| 
 | ||||
|   const onStackAssets = async () => { | ||||
|     if ($selectedAssets.size > 1) { | ||||
|       await stackAssets(Array.from($selectedAssets), (ids) => { | ||||
|         assetStore.removeAssets(ids); | ||||
|         dispatch('escape'); | ||||
|       }); | ||||
|     const ids = await stackAssets(Array.from($selectedAssets)); | ||||
|     if (ids) { | ||||
|       assetStore.removeAssets(ids); | ||||
|       dispatch('escape'); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; | ||||
| import { deleteAssets as deleteBulk } from '@immich/sdk'; | ||||
| import { deleteAssets as deleteBulk, type AssetResponseDto } from '@immich/sdk'; | ||||
| import { handleError } from './handle-error'; | ||||
| 
 | ||||
| export type OnDelete = (assetIds: string[]) => void; | ||||
| @ -7,6 +7,7 @@ export type OnRestore = (ids: string[]) => void; | ||||
| export type OnArchive = (ids: string[], isArchived: boolean) => void; | ||||
| export type OnFavorite = (ids: string[], favorite: boolean) => void; | ||||
| export type OnStack = (ids: string[]) => void; | ||||
| export type OnUnstack = (assets: AssetResponseDto[]) => void; | ||||
| 
 | ||||
| export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => { | ||||
|   try { | ||||
|  | ||||
| @ -2,6 +2,7 @@ import { goto } from '$app/navigation'; | ||||
| import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification'; | ||||
| import { AppRoute } from '$lib/constants'; | ||||
| import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; | ||||
| import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||
| import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; | ||||
| import { downloadManager } from '$lib/stores/download'; | ||||
| import { downloadRequest, getKey } from '$lib/utils'; | ||||
| @ -269,43 +270,81 @@ export const getSelectedAssets = (assets: Set<AssetResponseDto>, user: UserRespo | ||||
|   return ids; | ||||
| }; | ||||
| 
 | ||||
| export async function stackAssets(assets: Array<AssetResponseDto>, onStack: (ds: string[]) => void) { | ||||
| export const stackAssets = async (assets: AssetResponseDto[]) => { | ||||
|   if (assets.length < 2) { | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   const parent = assets[0]; | ||||
|   const children = assets.slice(1); | ||||
|   const ids = children.map(({ id }) => id); | ||||
| 
 | ||||
|   try { | ||||
|     const parent = assets.at(0); | ||||
|     if (!parent) { | ||||
|       return; | ||||
|     } | ||||
|     await updateAssets({ | ||||
|       assetBulkUpdateDto: { | ||||
|         ids, | ||||
|         stackParentId: parent.id, | ||||
|       }, | ||||
|     }); | ||||
|   } catch (error) { | ||||
|     handleError(error, 'Failed to stack assets'); | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|     const children = assets.slice(1); | ||||
|     const ids = children.map(({ id }) => id); | ||||
| 
 | ||||
|     if (children.length > 0) { | ||||
|       await updateAssets({ assetBulkUpdateDto: { ids, stackParentId: parent.id } }); | ||||
|     } | ||||
| 
 | ||||
|     let childrenCount = parent.stackCount || 1; | ||||
|     for (const asset of children) { | ||||
|       asset.stackParentId = parent.id; | ||||
|       // Add grand-children's count to new parent
 | ||||
|       childrenCount += asset.stackCount || 1; | ||||
|   let grandChildren: AssetResponseDto[] = []; | ||||
|   for (const asset of children) { | ||||
|     asset.stackParentId = parent.id; | ||||
|     if (asset.stack) { | ||||
|       // Add grand-children to new parent
 | ||||
|       grandChildren = grandChildren.concat(asset.stack); | ||||
|       // Reset children stack info
 | ||||
|       asset.stackCount = null; | ||||
|       asset.stack = []; | ||||
|     } | ||||
| 
 | ||||
|     parent.stackCount = childrenCount; | ||||
| 
 | ||||
|     notificationController.show({ | ||||
|       message: `Stacked ${ids.length + 1} assets`, | ||||
|       type: NotificationType.Info, | ||||
|       timeout: 1500, | ||||
|     }); | ||||
| 
 | ||||
|     onStack(ids); | ||||
|   } catch (error) { | ||||
|     handleError(error, `Unable to stack`); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|   parent.stack ??= []; | ||||
|   parent.stack = parent.stack.concat(children, grandChildren); | ||||
|   parent.stackCount = parent.stack.length + 1; | ||||
| 
 | ||||
|   notificationController.show({ | ||||
|     message: `Stacked ${parent.stackCount} assets`, | ||||
|     type: NotificationType.Info, | ||||
|     button: { | ||||
|       text: 'View Stack', | ||||
|       onClick() { | ||||
|         return assetViewingStore.setAssetId(parent.id); | ||||
|       }, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   return ids; | ||||
| }; | ||||
| 
 | ||||
| export const unstackAssets = async (assets: AssetResponseDto[]) => { | ||||
|   const ids = assets.map(({ id }) => id); | ||||
|   try { | ||||
|     await updateAssets({ | ||||
|       assetBulkUpdateDto: { | ||||
|         ids, | ||||
|         removeParent: true, | ||||
|       }, | ||||
|     }); | ||||
|   } catch (error) { | ||||
|     handleError(error, 'Failed to un-stack assets'); | ||||
|     return; | ||||
|   } | ||||
|   for (const asset of assets) { | ||||
|     asset.stackParentId = null; | ||||
|     asset.stackCount = null; | ||||
|     asset.stack = []; | ||||
|   } | ||||
|   notificationController.show({ | ||||
|     type: NotificationType.Info, | ||||
|     message: `Un-stacked ${assets.length} assets`, | ||||
|   }); | ||||
|   return assets; | ||||
| }; | ||||
| 
 | ||||
| export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => { | ||||
|   if (get(isSelectingAllAssets)) { | ||||
|  | ||||
| @ -30,7 +30,14 @@ | ||||
|   const assetInteractionStore = createAssetInteractionStore(); | ||||
|   const { isMultiSelectState, selectedAssets } = assetInteractionStore; | ||||
| 
 | ||||
|   $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite); | ||||
|   let isAllFavorite: boolean; | ||||
|   let isAssetStackSelected: boolean; | ||||
| 
 | ||||
|   $: { | ||||
|     const selection = [...$selectedAssets]; | ||||
|     isAllFavorite = selection.every((asset) => asset.isFavorite); | ||||
|     isAssetStackSelected = selection.length === 1 && !!selection[0].stack; | ||||
|   } | ||||
| 
 | ||||
|   const handleEscape = () => { | ||||
|     if ($showAssetViewer) { | ||||
| @ -62,8 +69,12 @@ | ||||
|     <FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> | ||||
|     <AssetSelectContextMenu icon={mdiDotsVertical} title="Menu"> | ||||
|       <DownloadAction menuItem /> | ||||
|       {#if $selectedAssets.size > 1} | ||||
|         <StackAction onStack={(assetIds) => assetStore.removeAssets(assetIds)} /> | ||||
|       {#if $selectedAssets.size > 1 || isAssetStackSelected} | ||||
|         <StackAction | ||||
|           unstack={isAssetStackSelected} | ||||
|           onStack={(assetIds) => assetStore.removeAssets(assetIds)} | ||||
|           onUnstack={(assets) => assetStore.addAssets(assets)} | ||||
|         /> | ||||
|       {/if} | ||||
|       <ChangeDate menuItem /> | ||||
|       <ChangeLocation menuItem /> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user