mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-30 10:24:58 -04:00 
			
		
		
		
	feat(web): Add action button to search result page (#2303)
* feat(web): Add action button to search result page * fix test * rename * pr feedback * better condition * fix test
This commit is contained in:
		
							parent
							
								
									13be271df7
								
							
						
					
					
						commit
						6f6f847ee2
					
				| @ -7,6 +7,7 @@ | ||||
| 	import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte'; | ||||
| 	import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte'; | ||||
| 	import Star from 'svelte-material-icons/Star.svelte'; | ||||
| 	import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte'; | ||||
| 	import ImageThumbnail from './image-thumbnail.svelte'; | ||||
| 	import VideoThumbnail from './video-thumbnail.svelte'; | ||||
| 
 | ||||
| @ -22,6 +23,7 @@ | ||||
| 	export let disabled = false; | ||||
| 	export let readonly = false; | ||||
| 	export let publicSharedKey: string | undefined = undefined; | ||||
| 	export let showArchiveIcon = false; | ||||
| 
 | ||||
| 	let mouseOver = false; | ||||
| 
 | ||||
| @ -114,6 +116,11 @@ | ||||
| 					</div> | ||||
| 				{/if} | ||||
| 
 | ||||
| 				{#if showArchiveIcon && asset.isArchived} | ||||
| 					<div class="absolute {asset.isFavorite ? 'bottom-10' : 'bottom-2'} left-2 z-10"> | ||||
| 						<ArchiveArrowDownOutline size="24" class="text-white" /> | ||||
| 					</div> | ||||
| 				{/if} | ||||
| 				<ImageThumbnail | ||||
| 					url={api.getAssetThumbnailUrl(asset.id, format, publicSharedKey)} | ||||
| 					altText={asset.originalFileName} | ||||
|  | ||||
| @ -22,6 +22,7 @@ | ||||
| 	export let selectedAssets: Set<AssetResponseDto> = new Set(); | ||||
| 	export let disableAssetSelect = false; | ||||
| 	export let viewFrom: ViewFrom; | ||||
| 	export let showArchiveIcon = false; | ||||
| 
 | ||||
| 	let isShowAssetViewer = false; | ||||
| 
 | ||||
| @ -141,6 +142,7 @@ | ||||
| 						on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))} | ||||
| 						on:select={selectAssetHandler} | ||||
| 						selected={selectedAssets.has(asset)} | ||||
| 						{showArchiveIcon} | ||||
| 					/> | ||||
| 				</div> | ||||
| 			{/each} | ||||
|  | ||||
| @ -7,7 +7,25 @@ | ||||
| 	import ImageOffOutline from 'svelte-material-icons/ImageOffOutline.svelte'; | ||||
| 	import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; | ||||
| 	import { afterNavigate, goto } from '$app/navigation'; | ||||
| 
 | ||||
| 	import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte'; | ||||
| 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||
| 	import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'; | ||||
| 	import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; | ||||
| 	import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'; | ||||
| 	import { | ||||
| 		notificationController, | ||||
| 		NotificationType | ||||
| 	} from '$lib/components/shared-components/notification/notification'; | ||||
| 	import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils'; | ||||
| 	import { AlbumResponseDto, api, AssetResponseDto, SharedLinkType } from '@api'; | ||||
| 	import Close from 'svelte-material-icons/Close.svelte'; | ||||
| 	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte'; | ||||
| 	import ArchiveArrowUpOutline from 'svelte-material-icons/ArchiveArrowUpOutline.svelte'; | ||||
| 	import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte'; | ||||
| 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; | ||||
| 	import Plus from 'svelte-material-icons/Plus.svelte'; | ||||
| 	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
| 	export let data: PageData; | ||||
| 
 | ||||
| 	// The GalleryViewer pushes it's own history state, which causes weird | ||||
| @ -23,14 +41,209 @@ | ||||
| 	}); | ||||
| 
 | ||||
| 	$: term = $page.url.searchParams.get('q') || data.term || ''; | ||||
| 
 | ||||
| 	let selectedAssets: Set<AssetResponseDto> = new Set(); | ||||
| 	$: isMultiSelectionMode = selectedAssets.size > 0; | ||||
| 	$: isAllArchived = Array.from(selectedAssets).every((asset) => asset.isArchived); | ||||
| 	$: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite); | ||||
| 
 | ||||
| 	let contextMenuPosition = { x: 0, y: 0 }; | ||||
| 	let isShowCreateSharedLinkModal = false; | ||||
| 	let isShowAddMenu = false; | ||||
| 	let isShowAlbumPicker = false; | ||||
| 	let addToSharedAlbum = false; | ||||
| 	$: searchResultAssets = data.results.assets.items; | ||||
| 
 | ||||
| 	const handleShowMenu = ({ x, y }: MouseEvent) => { | ||||
| 		contextMenuPosition = { x, y }; | ||||
| 		isShowAddMenu = !isShowAddMenu; | ||||
| 	}; | ||||
| 
 | ||||
| 	const handleShowAlbumPicker = (shared: boolean) => { | ||||
| 		isShowAddMenu = false; | ||||
| 		isShowAlbumPicker = true; | ||||
| 		addToSharedAlbum = shared; | ||||
| 	}; | ||||
| 
 | ||||
| 	const handleAddToNewAlbum = (event: CustomEvent) => { | ||||
| 		isShowAlbumPicker = false; | ||||
| 
 | ||||
| 		const { albumName }: { albumName: string } = event.detail; | ||||
| 		const assetIds = Array.from(selectedAssets).map((asset) => asset.id); | ||||
| 		api.albumApi.createAlbum({ albumName, assetIds }).then((response) => { | ||||
| 			const { id, albumName } = response.data; | ||||
| 
 | ||||
| 			notificationController.show({ | ||||
| 				message: `Added ${assetIds.length} to ${albumName}`, | ||||
| 				type: NotificationType.Info | ||||
| 			}); | ||||
| 
 | ||||
| 			clearMultiSelectAssetAssetHandler(); | ||||
| 
 | ||||
| 			goto('/albums/' + id); | ||||
| 		}); | ||||
| 	}; | ||||
| 
 | ||||
| 	const handleAddToAlbum = async (event: CustomEvent<{ album: AlbumResponseDto }>) => { | ||||
| 		isShowAlbumPicker = false; | ||||
| 		const album = event.detail.album; | ||||
| 
 | ||||
| 		const assetIds = Array.from(selectedAssets).map((asset) => asset.id); | ||||
| 
 | ||||
| 		addAssetsToAlbum(album.id, assetIds).then(() => { | ||||
| 			clearMultiSelectAssetAssetHandler(); | ||||
| 		}); | ||||
| 	}; | ||||
| 
 | ||||
| 	const handleDownloadFiles = async () => { | ||||
| 		await bulkDownload('immich', Array.from(selectedAssets), () => { | ||||
| 			clearMultiSelectAssetAssetHandler(); | ||||
| 		}); | ||||
| 	}; | ||||
| 
 | ||||
| 	const toggleArchive = async () => { | ||||
| 		let cnt = 0; | ||||
| 		for (const asset of selectedAssets) { | ||||
| 			api.assetApi.updateAsset(asset.id, { | ||||
| 				isArchived: !isAllArchived | ||||
| 			}); | ||||
| 			cnt = cnt + 1; | ||||
| 
 | ||||
| 			asset.isArchived = !isAllArchived; | ||||
| 
 | ||||
| 			searchResultAssets = searchResultAssets.map((a: AssetResponseDto) => { | ||||
| 				if (a.id === asset.id) { | ||||
| 					a = asset; | ||||
| 				} | ||||
| 
 | ||||
| 				return a; | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		notificationController.show({ | ||||
| 			message: `${isAllArchived ? `Remove ${cnt} from` : `Add ${cnt} to`} archive`, | ||||
| 			type: NotificationType.Info | ||||
| 		}); | ||||
| 
 | ||||
| 		clearMultiSelectAssetAssetHandler(); | ||||
| 	}; | ||||
| 
 | ||||
| 	const toggleFavorite = () => { | ||||
| 		isShowAddMenu = false; | ||||
| 
 | ||||
| 		let cnt = 0; | ||||
| 		for (const asset of selectedAssets) { | ||||
| 			api.assetApi.updateAsset(asset.id, { | ||||
| 				isFavorite: !isAllFavorite | ||||
| 			}); | ||||
| 			cnt = cnt + 1; | ||||
| 
 | ||||
| 			asset.isFavorite = !isAllFavorite; | ||||
| 
 | ||||
| 			searchResultAssets = searchResultAssets.map((a: AssetResponseDto) => { | ||||
| 				if (a.id === asset.id) { | ||||
| 					a = asset; | ||||
| 				} | ||||
| 				return a; | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		notificationController.show({ | ||||
| 			message: `${isAllFavorite ? `Remove ${cnt} from` : `Add ${cnt} to`} favorites`, | ||||
| 			type: NotificationType.Info | ||||
| 		}); | ||||
| 
 | ||||
| 		clearMultiSelectAssetAssetHandler(); | ||||
| 	}; | ||||
| 
 | ||||
| 	const clearMultiSelectAssetAssetHandler = () => { | ||||
| 		selectedAssets = new Set(); | ||||
| 	}; | ||||
| 
 | ||||
| 	const deleteSelectedAssetHandler = async () => { | ||||
| 		try { | ||||
| 			if ( | ||||
| 				window.confirm( | ||||
| 					`Caution! Are you sure you want to delete ${selectedAssets.size} assets? This step also deletes assets in the album(s) to which they belong. You can not undo this action!` | ||||
| 				) | ||||
| 			) { | ||||
| 				const { data: deletedAssets } = await api.assetApi.deleteAsset({ | ||||
| 					ids: Array.from(selectedAssets).map((a) => a.id) | ||||
| 				}); | ||||
| 
 | ||||
| 				for (const asset of deletedAssets) { | ||||
| 					if (asset.status == 'SUCCESS') { | ||||
| 						searchResultAssets = searchResultAssets.filter( | ||||
| 							(a: AssetResponseDto) => a.id != asset.id | ||||
| 						); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				clearMultiSelectAssetAssetHandler(); | ||||
| 			} | ||||
| 		} catch (e) { | ||||
| 			notificationController.show({ | ||||
| 				type: NotificationType.Error, | ||||
| 				message: 'Error deleting assets, check console for more details' | ||||
| 			}); | ||||
| 			console.error('Error deleteSelectedAssetHandler', e); | ||||
| 		} | ||||
| 	}; | ||||
| 	const handleCreateSharedLink = async () => { | ||||
| 		isShowCreateSharedLinkModal = true; | ||||
| 	}; | ||||
| 
 | ||||
| 	const handleCloseSharedLinkModal = () => { | ||||
| 		clearMultiSelectAssetAssetHandler(); | ||||
| 		isShowCreateSharedLinkModal = false; | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| <section> | ||||
| 	{#if isMultiSelectionMode} | ||||
| 		<ControlAppBar | ||||
| 			on:close-button-click={clearMultiSelectAssetAssetHandler} | ||||
| 			backIcon={Close} | ||||
| 			tailwindClasses={'bg-white shadow-md'} | ||||
| 		> | ||||
| 			<svelte:fragment slot="leading"> | ||||
| 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | ||||
| 					Selected {selectedAssets.size.toLocaleString($locale)} | ||||
| 				</p> | ||||
| 			</svelte:fragment> | ||||
| 			<svelte:fragment slot="trailing"> | ||||
| 				<CircleIconButton | ||||
| 					title="Share" | ||||
| 					logo={ShareVariantOutline} | ||||
| 					on:click={handleCreateSharedLink} | ||||
| 				/> | ||||
| 
 | ||||
| 				<CircleIconButton | ||||
| 					title={isAllArchived ? 'Unarchive' : 'Archive'} | ||||
| 					logo={isAllArchived ? ArchiveArrowUpOutline : ArchiveArrowDownOutline} | ||||
| 					on:click={toggleArchive} | ||||
| 				/> | ||||
| 
 | ||||
| 				<CircleIconButton | ||||
| 					title="Download" | ||||
| 					logo={CloudDownloadOutline} | ||||
| 					on:click={handleDownloadFiles} | ||||
| 				/> | ||||
| 				<CircleIconButton title="Add" logo={Plus} on:click={handleShowMenu} /> | ||||
| 				<CircleIconButton | ||||
| 					title="Delete" | ||||
| 					logo={DeleteOutline} | ||||
| 					on:click={deleteSelectedAssetHandler} | ||||
| 				/> | ||||
| 			</svelte:fragment> | ||||
| 		</ControlAppBar> | ||||
| 	{:else} | ||||
| 		<ControlAppBar on:close-button-click={() => goto(previousRoute)} backIcon={ArrowLeft}> | ||||
| 			<div class="w-full max-w-2xl flex-1 pl-4"> | ||||
| 				<SearchBar grayTheme={false} value={term} /> | ||||
| 			</div> | ||||
| 		</ControlAppBar> | ||||
| 	{/if} | ||||
| </section> | ||||
| 
 | ||||
| <section class="relative pt-32 mb-12 bg-immich-bg dark:bg-immich-dark-bg"> | ||||
| @ -39,9 +252,10 @@ | ||||
| 			{#if data.results?.assets?.items.length > 0} | ||||
| 				<div class="pl-4"> | ||||
| 					<GalleryViewer | ||||
| 						assets={data.results.assets.items} | ||||
| 						disableAssetSelect | ||||
| 						assets={searchResultAssets} | ||||
| 						bind:selectedAssets | ||||
| 						viewFrom="search-page" | ||||
| 						showArchiveIcon={true} | ||||
| 					/> | ||||
| 				</div> | ||||
| 			{:else} | ||||
| @ -57,4 +271,35 @@ | ||||
| 			{/if} | ||||
| 		</section> | ||||
| 	</section> | ||||
| 
 | ||||
| 	{#if isShowAddMenu} | ||||
| 		<ContextMenu {...contextMenuPosition} on:clickoutside={() => (isShowAddMenu = false)}> | ||||
| 			<div class="flex flex-col rounded-lg "> | ||||
| 				<MenuOption | ||||
| 					on:click={toggleFavorite} | ||||
| 					text={isAllFavorite ? 'Remove from favorites' : 'Add to favorites'} | ||||
| 				/> | ||||
| 				<MenuOption on:click={() => handleShowAlbumPicker(false)} text="Add to Album" /> | ||||
| 				<MenuOption on:click={() => handleShowAlbumPicker(true)} text="Add to Shared Album" /> | ||||
| 			</div> | ||||
| 		</ContextMenu> | ||||
| 	{/if} | ||||
| 
 | ||||
| 	{#if isShowAlbumPicker} | ||||
| 		<AlbumSelectionModal | ||||
| 			shared={addToSharedAlbum} | ||||
| 			on:newAlbum={handleAddToNewAlbum} | ||||
| 			on:newSharedAlbum={handleAddToNewAlbum} | ||||
| 			on:album={handleAddToAlbum} | ||||
| 			on:close={() => (isShowAlbumPicker = false)} | ||||
| 		/> | ||||
| 	{/if} | ||||
| 
 | ||||
| 	{#if isShowCreateSharedLinkModal} | ||||
| 		<CreateSharedLinkModal | ||||
| 			sharedAssets={Array.from(selectedAssets)} | ||||
| 			shareType={SharedLinkType.Individual} | ||||
| 			on:close={handleCloseSharedLinkModal} | ||||
| 		/> | ||||
| 	{/if} | ||||
| </section> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user