mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:37:11 -04:00 
			
		
		
		
	feat(web): Add upload to stack action (#19842)
* feat(web): Add upload to stack action * Event handling and translation * Update asset viewer instead * lint, improve upload return type * Add suggestions from code review * Resolve merge conflicts * Apply suggestions from code review
This commit is contained in:
		
							parent
							
								
									d764a59011
								
							
						
					
					
						commit
						cf60f4cdcd
					
				| @ -33,6 +33,7 @@ | |||||||
|   "add_to_albums": "Add to albums", |   "add_to_albums": "Add to albums", | ||||||
|   "add_to_albums_count": "Add to albums ({count})", |   "add_to_albums_count": "Add to albums ({count})", | ||||||
|   "add_to_shared_album": "Add to shared album", |   "add_to_shared_album": "Add to shared album", | ||||||
|  |   "add_upload_to_stack": "Add upload to stack", | ||||||
|   "add_url": "Add URL", |   "add_url": "Add URL", | ||||||
|   "added_to_archive": "Added to archive", |   "added_to_archive": "Added to archive", | ||||||
|   "added_to_favorites": "Added to favorites", |   "added_to_favorites": "Added to favorites", | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ type ActionMap = { | |||||||
|   [AssetAction.RESTORE]: { asset: TimelineAsset }; |   [AssetAction.RESTORE]: { asset: TimelineAsset }; | ||||||
|   [AssetAction.ADD]: { asset: TimelineAsset }; |   [AssetAction.ADD]: { asset: TimelineAsset }; | ||||||
|   [AssetAction.ADD_TO_ALBUM]: { asset: TimelineAsset; album: AlbumResponseDto }; |   [AssetAction.ADD_TO_ALBUM]: { asset: TimelineAsset; album: AlbumResponseDto }; | ||||||
|  |   [AssetAction.STACK]: { stack: StackResponseDto }; | ||||||
|   [AssetAction.UNSTACK]: { assets: TimelineAsset[] }; |   [AssetAction.UNSTACK]: { assets: TimelineAsset[] }; | ||||||
|   [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: TimelineAsset }; |   [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: TimelineAsset }; | ||||||
|   [AssetAction.SET_STACK_PRIMARY_ASSET]: { stack: StackResponseDto }; |   [AssetAction.SET_STACK_PRIMARY_ASSET]: { stack: StackResponseDto }; | ||||||
|  | |||||||
| @ -0,0 +1,37 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; | ||||||
|  |   import { AssetAction } from '$lib/constants'; | ||||||
|  |   import { openFileUploadDialog } from '$lib/utils/file-uploader'; | ||||||
|  |   import { createStack, type AssetResponseDto, type StackResponseDto } from '@immich/sdk'; | ||||||
|  |   import { mdiUploadMultiple } from '@mdi/js'; | ||||||
|  |   import { t } from 'svelte-i18n'; | ||||||
|  |   import type { OnAction } from './action'; | ||||||
|  | 
 | ||||||
|  |   interface Props { | ||||||
|  |     asset: AssetResponseDto; | ||||||
|  |     stack: StackResponseDto | null; | ||||||
|  |     onAction: OnAction; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let { asset, stack, onAction }: Props = $props(); | ||||||
|  | 
 | ||||||
|  |   const handleAddUploadToStack = async () => { | ||||||
|  |     const newAssetIds = await openFileUploadDialog({ multiple: true }); | ||||||
|  |     // Including the old stacks primary asset ID ensures that all assets of the | ||||||
|  |     // old stack are automatically included in the new stack. | ||||||
|  |     const primaryAssetId = stack?.primaryAssetId ?? asset.id; | ||||||
|  | 
 | ||||||
|  |     // First asset in the list will become the new primary asset. | ||||||
|  |     const assetIds = [primaryAssetId, ...newAssetIds]; | ||||||
|  | 
 | ||||||
|  |     const newStack = await createStack({ | ||||||
|  |       stackCreateDto: { | ||||||
|  |         assetIds, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     onAction({ type: AssetAction.STACK, stack: newStack }); | ||||||
|  |   }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <MenuOption icon={mdiUploadMultiple} onClick={handleAddUploadToStack} text={$t('add_upload_to_stack')} /> | ||||||
| @ -4,6 +4,7 @@ | |||||||
|   import CastButton from '$lib/cast/cast-button.svelte'; |   import CastButton from '$lib/cast/cast-button.svelte'; | ||||||
|   import type { OnAction, PreAction } from '$lib/components/asset-viewer/actions/action'; |   import type { OnAction, PreAction } from '$lib/components/asset-viewer/actions/action'; | ||||||
|   import AddToAlbumAction from '$lib/components/asset-viewer/actions/add-to-album-action.svelte'; |   import AddToAlbumAction from '$lib/components/asset-viewer/actions/add-to-album-action.svelte'; | ||||||
|  |   import AddToStackAction from '$lib/components/asset-viewer/actions/add-to-stack-action.svelte'; | ||||||
|   import ArchiveAction from '$lib/components/asset-viewer/actions/archive-action.svelte'; |   import ArchiveAction from '$lib/components/asset-viewer/actions/archive-action.svelte'; | ||||||
|   import CloseAction from '$lib/components/asset-viewer/actions/close-action.svelte'; |   import CloseAction from '$lib/components/asset-viewer/actions/close-action.svelte'; | ||||||
|   import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte'; |   import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte'; | ||||||
| @ -196,6 +197,7 @@ | |||||||
|         {/if} |         {/if} | ||||||
| 
 | 
 | ||||||
|         {#if isOwner} |         {#if isOwner} | ||||||
|  |           <AddToStackAction {asset} {stack} {onAction} /> | ||||||
|           {#if stack} |           {#if stack} | ||||||
|             <UnstackAction {stack} {onAction} /> |             <UnstackAction {stack} {onAction} /> | ||||||
|             <KeepThisDeleteOthersAction {stack} {asset} {onAction} /> |             <KeepThisDeleteOthersAction {stack} {asset} {onAction} /> | ||||||
|  | |||||||
| @ -336,6 +336,7 @@ | |||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|  |       case AssetAction.STACK: | ||||||
|       case AssetAction.SET_STACK_PRIMARY_ASSET: { |       case AssetAction.SET_STACK_PRIMARY_ASSET: { | ||||||
|         stack = action.stack; |         stack = action.stack; | ||||||
|         break; |         break; | ||||||
|  | |||||||
| @ -120,6 +120,16 @@ | |||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       case AssetAction.STACK: { | ||||||
|  |         updateStackedAssetInTimeline(timelineManager, { | ||||||
|  |           stack: action.stack, | ||||||
|  |           toDeleteIds: action.stack.assets | ||||||
|  |             .filter((asset) => asset.id !== action.stack.primaryAssetId) | ||||||
|  |             .map((asset) => asset.id), | ||||||
|  |         }); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       case AssetAction.UNSTACK: { |       case AssetAction.UNSTACK: { | ||||||
|         updateUnstackedAssetInTimeline(timelineManager, action.assets); |         updateUnstackedAssetInTimeline(timelineManager, action.assets); | ||||||
|         break; |         break; | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ export enum AssetAction { | |||||||
|   RESTORE = 'restore', |   RESTORE = 'restore', | ||||||
|   ADD = 'add', |   ADD = 'add', | ||||||
|   ADD_TO_ALBUM = 'add-to-album', |   ADD_TO_ALBUM = 'add-to-album', | ||||||
|  |   STACK = 'stack', | ||||||
|   UNSTACK = 'unstack', |   UNSTACK = 'unstack', | ||||||
|   KEEP_THIS_DELETE_OTHERS = 'keep-this-delete-others', |   KEEP_THIS_DELETE_OTHERS = 'keep-this-delete-others', | ||||||
|   SET_STACK_PRIMARY_ASSET = 'set-stack-primary-asset', |   SET_STACK_PRIMARY_ASSET = 'set-stack-primary-asset', | ||||||
|  | |||||||
| @ -52,7 +52,7 @@ export const openFileUploadDialog = async (options: FileUploadParam = {}) => { | |||||||
|   const { albumId, multiple = true, assetId } = options; |   const { albumId, multiple = true, assetId } = options; | ||||||
|   const extensions = uploadManager.getExtensions(); |   const extensions = uploadManager.getExtensions(); | ||||||
| 
 | 
 | ||||||
|   return new Promise<(string | undefined)[]>((resolve, reject) => { |   return new Promise<string[]>((resolve, reject) => { | ||||||
|     try { |     try { | ||||||
|       const fileSelector = document.createElement('input'); |       const fileSelector = document.createElement('input'); | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user