mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:49:11 -04:00 
			
		
		
		
	refactor(web): asset select actions (#2444)
* refactor(web): asset select actions * remaining pages/components + data flow changes * fix check
This commit is contained in:
		
							parent
							
								
									3ec74444b0
								
							
						
					
					
						commit
						ab86d0a18d
					
				| @ -1,49 +1,48 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | 	import { browser } from '$app/environment'; | ||||||
| 	import { afterNavigate, goto } from '$app/navigation'; | 	import { afterNavigate, goto } from '$app/navigation'; | ||||||
|  | 	import { albumAssetSelectionStore } from '$lib/stores/album-asset-selection.store'; | ||||||
|  | 	import { downloadAssets } from '$lib/stores/download'; | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
|  | 	import { clickOutside } from '$lib/utils/click-outside'; | ||||||
|  | 	import { openFileUploadDialog } from '$lib/utils/file-uploader'; | ||||||
| 	import { | 	import { | ||||||
| 		AlbumResponseDto, | 		AlbumResponseDto, | ||||||
| 		api, |  | ||||||
| 		AssetResponseDto, | 		AssetResponseDto, | ||||||
| 		SharedLinkResponseDto, | 		SharedLinkResponseDto, | ||||||
| 		SharedLinkType, | 		SharedLinkType, | ||||||
| 		UserResponseDto | 		UserResponseDto, | ||||||
|  | 		api | ||||||
| 	} from '@api'; | 	} from '@api'; | ||||||
| 	import { onMount } from 'svelte'; | 	import { onMount } from 'svelte'; | ||||||
| 	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; | 	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; | ||||||
| 	import Plus from 'svelte-material-icons/Plus.svelte'; |  | ||||||
| 	import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte'; |  | ||||||
| 	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; |  | ||||||
| 	import CircleAvatar from '../shared-components/circle-avatar.svelte'; |  | ||||||
| 	import AssetSelection from './asset-selection.svelte'; |  | ||||||
| 	import UserSelectionModal from './user-selection-modal.svelte'; |  | ||||||
| 	import ShareInfoModal from './share-info-modal.svelte'; |  | ||||||
| 	import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; |  | ||||||
| 	import Close from 'svelte-material-icons/Close.svelte'; |  | ||||||
| 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; | 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; | ||||||
| 	import FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte'; |  | ||||||
| 	import { downloadAssets } from '$lib/stores/download'; |  | ||||||
| 	import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; | 	import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; | ||||||
|  | 	import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte'; | ||||||
|  | 	import FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte'; | ||||||
|  | 	import Plus from 'svelte-material-icons/Plus.svelte'; | ||||||
|  | 	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; | ||||||
|  | 	import Button from '../elements/buttons/button.svelte'; | ||||||
|  | 	import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | ||||||
|  | 	import DownloadFiles from '../photos-page/actions/download-files.svelte'; | ||||||
|  | 	import RemoveFromAlbum from '../photos-page/actions/remove-from-album.svelte'; | ||||||
|  | 	import AssetSelectControlBar from '../photos-page/asset-select-control-bar.svelte'; | ||||||
|  | 	import CircleAvatar from '../shared-components/circle-avatar.svelte'; | ||||||
| 	import ContextMenu from '../shared-components/context-menu/context-menu.svelte'; | 	import ContextMenu from '../shared-components/context-menu/context-menu.svelte'; | ||||||
| 	import MenuOption from '../shared-components/context-menu/menu-option.svelte'; | 	import MenuOption from '../shared-components/context-menu/menu-option.svelte'; | ||||||
| 	import ThumbnailSelection from './thumbnail-selection.svelte'; |  | ||||||
| 	import ControlAppBar from '../shared-components/control-app-bar.svelte'; | 	import ControlAppBar from '../shared-components/control-app-bar.svelte'; | ||||||
| 	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte'; |  | ||||||
| 
 |  | ||||||
| 	import { |  | ||||||
| 		notificationController, |  | ||||||
| 		NotificationType |  | ||||||
| 	} from '../shared-components/notification/notification'; |  | ||||||
| 	import { browser } from '$app/environment'; |  | ||||||
| 	import { albumAssetSelectionStore } from '$lib/stores/album-asset-selection.store'; |  | ||||||
| 	import CreateSharedLinkModal from '../shared-components/create-share-link-modal/create-shared-link-modal.svelte'; | 	import CreateSharedLinkModal from '../shared-components/create-share-link-modal/create-shared-link-modal.svelte'; | ||||||
| 	import ThemeButton from '../shared-components/theme-button.svelte'; |  | ||||||
| 	import { openFileUploadDialog } from '$lib/utils/file-uploader'; |  | ||||||
| 	import { bulkDownload } from '$lib/utils/asset-utils'; |  | ||||||
| 	import { locale } from '$lib/stores/preferences.store'; |  | ||||||
| 	import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; | 	import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; | ||||||
| 	import ImmichLogo from '../shared-components/immich-logo.svelte'; | 	import ImmichLogo from '../shared-components/immich-logo.svelte'; | ||||||
| 	import Button from '../elements/buttons/button.svelte'; | 	import { | ||||||
| 	import { clickOutside } from '$lib/utils/click-outside'; | 		NotificationType, | ||||||
|  | 		notificationController | ||||||
|  | 	} from '../shared-components/notification/notification'; | ||||||
|  | 	import ThemeButton from '../shared-components/theme-button.svelte'; | ||||||
|  | 	import AssetSelection from './asset-selection.svelte'; | ||||||
|  | 	import ShareInfoModal from './share-info-modal.svelte'; | ||||||
|  | 	import ThumbnailSelection from './thumbnail-selection.svelte'; | ||||||
|  | 	import UserSelectionModal from './user-selection-modal.svelte'; | ||||||
| 
 | 
 | ||||||
| 	export let album: AlbumResponseDto; | 	export let album: AlbumResponseDto; | ||||||
| 	export let sharedLink: SharedLinkResponseDto | undefined = undefined; | 	export let sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||||
| @ -125,25 +124,6 @@ | |||||||
| 		multiSelectAsset = new Set(); | 		multiSelectAsset = new Set(); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const removeSelectedAssetFromAlbum = async () => { |  | ||||||
| 		if (window.confirm('Do you want to remove selected assets from the album?')) { |  | ||||||
| 			try { |  | ||||||
| 				const { data } = await api.albumApi.removeAssetFromAlbum(album.id, { |  | ||||||
| 					assetIds: Array.from(multiSelectAsset).map((a) => a.id) |  | ||||||
| 				}); |  | ||||||
| 
 |  | ||||||
| 				album = data; |  | ||||||
| 				multiSelectAsset = new Set(); |  | ||||||
| 			} catch (e) { |  | ||||||
| 				console.error('Error [album-viewer] [removeAssetFromAlbum]', e); |  | ||||||
| 				notificationController.show({ |  | ||||||
| 					type: NotificationType.Error, |  | ||||||
| 					message: 'Error removing assets from album, check console for more details' |  | ||||||
| 				}); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	// Update Album Name | 	// Update Album Name | ||||||
| 	$: { | 	$: { | ||||||
| 		if (!isEditingTitle && currentAlbumName != album.albumName && isOwned) { | 		if (!isEditingTitle && currentAlbumName != album.albumName && isOwned) { | ||||||
| @ -353,48 +333,20 @@ | |||||||
| 		isShowShareUserSelection = false; | 		isShowShareUserSelection = false; | ||||||
| 		isShowShareLinkModal = true; | 		isShowShareLinkModal = true; | ||||||
| 	}; | 	}; | ||||||
| 
 |  | ||||||
| 	const handleDownloadSelectedAssets = async () => { |  | ||||||
| 		await bulkDownload( |  | ||||||
| 			album.albumName, |  | ||||||
| 			Array.from(multiSelectAsset), |  | ||||||
| 			() => { |  | ||||||
| 				isMultiSelectionMode = false; |  | ||||||
| 				clearMultiSelectAssetAssetHandler(); |  | ||||||
| 			}, |  | ||||||
| 			sharedLink?.key |  | ||||||
| 		); |  | ||||||
| 	}; |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <section class="bg-immich-bg dark:bg-immich-dark-bg" class:hidden={isShowThumbnailSelection}> | <section class="bg-immich-bg dark:bg-immich-dark-bg" class:hidden={isShowThumbnailSelection}> | ||||||
| 	<!-- Multiselection mode app bar --> | 	<!-- Multiselection mode app bar --> | ||||||
| 	{#if isMultiSelectionMode} | 	{#if isMultiSelectionMode} | ||||||
| 		<ControlAppBar | 		<AssetSelectControlBar | ||||||
| 			on:close-button-click={clearMultiSelectAssetAssetHandler} | 			assets={multiSelectAsset} | ||||||
| 			backIcon={Close} | 			clearSelect={clearMultiSelectAssetAssetHandler} | ||||||
| 			tailwindClasses={'bg-white shadow-md'} |  | ||||||
| 		> | 		> | ||||||
| 			<svelte:fragment slot="leading"> | 			<DownloadFiles filename={album.albumName} sharedLinkKey={sharedLink?.key} /> | ||||||
| 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | 			{#if isOwned} | ||||||
| 					Selected {multiSelectAsset.size.toLocaleString($locale)} | 				<RemoveFromAlbum bind:album /> | ||||||
| 				</p> | 			{/if} | ||||||
| 			</svelte:fragment> | 		</AssetSelectControlBar> | ||||||
| 			<svelte:fragment slot="trailing"> |  | ||||||
| 				<CircleIconButton |  | ||||||
| 					title="Download" |  | ||||||
| 					on:click={handleDownloadSelectedAssets} |  | ||||||
| 					logo={CloudDownloadOutline} |  | ||||||
| 				/> |  | ||||||
| 				{#if isOwned} |  | ||||||
| 					<CircleIconButton |  | ||||||
| 						title="Remove from album" |  | ||||||
| 						on:click={removeSelectedAssetFromAlbum} |  | ||||||
| 						logo={DeleteOutline} |  | ||||||
| 					/> |  | ||||||
| 				{/if} |  | ||||||
| 			</svelte:fragment> |  | ||||||
| 		</ControlAppBar> |  | ||||||
| 	{/if} | 	{/if} | ||||||
| 
 | 
 | ||||||
| 	<!-- Default app bar --> | 	<!-- Default app bar --> | ||||||
|  | |||||||
| @ -0,0 +1,23 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||||
|  | 	import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'; | ||||||
|  | 	import { SharedLinkType } from '@api'; | ||||||
|  | 	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; | ||||||
|  | 	import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|  | 
 | ||||||
|  | 	let showModal = false; | ||||||
|  | 	const { getAssets, clearSelect } = getAssetControlContext(); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <CircleIconButton title="Share" logo={ShareVariantOutline} on:click={() => (showModal = true)} /> | ||||||
|  | 
 | ||||||
|  | {#if showModal} | ||||||
|  | 	<CreateSharedLinkModal | ||||||
|  | 		sharedAssets={Array.from(getAssets())} | ||||||
|  | 		shareType={SharedLinkType.Individual} | ||||||
|  | 		on:close={() => { | ||||||
|  | 			showModal = false; | ||||||
|  | 			clearSelect(); | ||||||
|  | 		}} | ||||||
|  | 	/> | ||||||
|  | {/if} | ||||||
| @ -0,0 +1,45 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||||
|  | 	import { | ||||||
|  | 		NotificationType, | ||||||
|  | 		notificationController | ||||||
|  | 	} from '$lib/components/shared-components/notification/notification'; | ||||||
|  | 	import { api } from '@api'; | ||||||
|  | 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; | ||||||
|  | 	import { OnAssetDelete, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let onAssetDelete: OnAssetDelete; | ||||||
|  | 	const { getAssets, clearSelect } = getAssetControlContext(); | ||||||
|  | 
 | ||||||
|  | 	const deleteSelectedAssetHandler = async () => { | ||||||
|  | 		try { | ||||||
|  | 			if ( | ||||||
|  | 				window.confirm( | ||||||
|  | 					`Caution! Are you sure you want to delete ${ | ||||||
|  | 						getAssets().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(getAssets()).map((a) => a.id) | ||||||
|  | 				}); | ||||||
|  | 
 | ||||||
|  | 				for (const asset of deletedAssets) { | ||||||
|  | 					if (asset.status === 'SUCCESS') { | ||||||
|  | 						onAssetDelete(asset.id); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				clearSelect(); | ||||||
|  | 			} | ||||||
|  | 		} catch (e) { | ||||||
|  | 			notificationController.show({ | ||||||
|  | 				type: NotificationType.Error, | ||||||
|  | 				message: 'Error deleting assets, check console for more details' | ||||||
|  | 			}); | ||||||
|  | 			console.error('Error deleteSelectedAssetHandler', e); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <CircleIconButton title="Delete" logo={DeleteOutline} on:click={deleteSelectedAssetHandler} /> | ||||||
| @ -0,0 +1,17 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||||
|  | 	import { bulkDownload } from '$lib/utils/asset-utils'; | ||||||
|  | 	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte'; | ||||||
|  | 	import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let filename = 'immich'; | ||||||
|  | 	export let sharedLinkKey: string | undefined = undefined; | ||||||
|  | 
 | ||||||
|  | 	const { getAssets, clearSelect } = getAssetControlContext(); | ||||||
|  | 
 | ||||||
|  | 	const handleDownloadFiles = async () => { | ||||||
|  | 		await bulkDownload(filename, Array.from(getAssets()), clearSelect, sharedLinkKey); | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <CircleIconButton title="Download" logo={CloudDownloadOutline} on:click={handleDownloadFiles} /> | ||||||
| @ -0,0 +1,40 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||||
|  | 	import { | ||||||
|  | 		NotificationType, | ||||||
|  | 		notificationController | ||||||
|  | 	} from '$lib/components/shared-components/notification/notification'; | ||||||
|  | 	import { api } from '@api'; | ||||||
|  | 	import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte'; | ||||||
|  | 	import { OnAssetArchive, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let onAssetArchive: OnAssetArchive = (asset, archive) => { | ||||||
|  | 		asset.isArchived = archive; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const { getAssets, clearSelect } = getAssetControlContext(); | ||||||
|  | 
 | ||||||
|  | 	const handleArchive = async () => { | ||||||
|  | 		let cnt = 0; | ||||||
|  | 
 | ||||||
|  | 		for (const asset of getAssets()) { | ||||||
|  | 			if (!asset.isArchived) { | ||||||
|  | 				api.assetApi.updateAsset(asset.id, { | ||||||
|  | 					isArchived: true | ||||||
|  | 				}); | ||||||
|  | 
 | ||||||
|  | 				onAssetArchive(asset, true); | ||||||
|  | 				cnt = cnt + 1; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		notificationController.show({ | ||||||
|  | 			message: `Archived ${cnt}`, | ||||||
|  | 			type: NotificationType.Info | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		clearSelect(); | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <CircleIconButton title="Archive" logo={ArchiveArrowDownOutline} on:click={handleArchive} /> | ||||||
| @ -0,0 +1,36 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||||
|  | 	import { handleError } from '$lib/utils/handle-error'; | ||||||
|  | 	import { api } from '@api'; | ||||||
|  | 	import HeartMinusOutline from 'svelte-material-icons/HeartMinusOutline.svelte'; | ||||||
|  | 	import { getAssetControlContext, OnAssetFavorite } from '../asset-select-control-bar.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let onAssetFavorite: OnAssetFavorite = (asset, favorite) => { | ||||||
|  | 		asset.isFavorite = favorite; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const { getAssets, clearSelect } = getAssetControlContext(); | ||||||
|  | 
 | ||||||
|  | 	const handleRemoveFavorite = async () => { | ||||||
|  | 		for (const asset of getAssets()) { | ||||||
|  | 			try { | ||||||
|  | 				await api.assetApi.updateAsset(asset.id, { | ||||||
|  | 					isFavorite: false | ||||||
|  | 				}); | ||||||
|  | 				onAssetFavorite(asset, false); | ||||||
|  | 			} catch { | ||||||
|  | 				handleError(Error, 'Error updating asset favorite state'); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		clearSelect(); | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <slot {handleRemoveFavorite}> | ||||||
|  | 	<CircleIconButton | ||||||
|  | 		title="Remove Favorite" | ||||||
|  | 		logo={HeartMinusOutline} | ||||||
|  | 		on:click={handleRemoveFavorite} | ||||||
|  | 	/> | ||||||
|  | </slot> | ||||||
| @ -0,0 +1,35 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||||
|  | 	import { | ||||||
|  | 		NotificationType, | ||||||
|  | 		notificationController | ||||||
|  | 	} from '$lib/components/shared-components/notification/notification'; | ||||||
|  | 	import { AlbumResponseDto, api } from '@api'; | ||||||
|  | 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; | ||||||
|  | 	import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let album: AlbumResponseDto; | ||||||
|  | 
 | ||||||
|  | 	const { getAssets, clearSelect } = getAssetControlContext(); | ||||||
|  | 
 | ||||||
|  | 	const handleRemoveFromAlbum = async () => { | ||||||
|  | 		if (window.confirm('Do you want to remove selected assets from the album?')) { | ||||||
|  | 			try { | ||||||
|  | 				const { data } = await api.albumApi.removeAssetFromAlbum(album.id, { | ||||||
|  | 					assetIds: Array.from(getAssets()).map((a) => a.id) | ||||||
|  | 				}); | ||||||
|  | 
 | ||||||
|  | 				album = data; | ||||||
|  | 				clearSelect(); | ||||||
|  | 			} catch (e) { | ||||||
|  | 				console.error('Error [album-viewer] [removeAssetFromAlbum]', e); | ||||||
|  | 				notificationController.show({ | ||||||
|  | 					type: NotificationType.Error, | ||||||
|  | 					message: 'Error removing assets from album, check console for more details' | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <CircleIconButton title="Remove from album" on:click={handleRemoveFromAlbum} logo={DeleteOutline} /> | ||||||
| @ -0,0 +1,39 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||||
|  | 	import { | ||||||
|  | 		NotificationType, | ||||||
|  | 		notificationController | ||||||
|  | 	} from '$lib/components/shared-components/notification/notification'; | ||||||
|  | 	import { api } from '@api'; | ||||||
|  | 	import ArchiveArrowUpOutline from 'svelte-material-icons/ArchiveArrowUpOutline.svelte'; | ||||||
|  | 	import { OnAssetArchive, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let onAssetArchive: OnAssetArchive = (asset, archived) => { | ||||||
|  | 		asset.isArchived = archived; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const { getAssets, clearSelect } = getAssetControlContext(); | ||||||
|  | 
 | ||||||
|  | 	const handleUnarchive = async () => { | ||||||
|  | 		let cnt = 0; | ||||||
|  | 		for (const asset of getAssets()) { | ||||||
|  | 			if (asset.isArchived) { | ||||||
|  | 				api.assetApi.updateAsset(asset.id, { | ||||||
|  | 					isArchived: false | ||||||
|  | 				}); | ||||||
|  | 
 | ||||||
|  | 				onAssetArchive(asset, false); | ||||||
|  | 				cnt = cnt + 1; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		notificationController.show({ | ||||||
|  | 			message: `Removed ${cnt} from archive`, | ||||||
|  | 			type: NotificationType.Info | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		clearSelect(); | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <CircleIconButton title="Unarchive" logo={ArchiveArrowUpOutline} on:click={handleUnarchive} /> | ||||||
| @ -0,0 +1,34 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||||
|  | 	import { AssetResponseDto, SharedLinkResponseDto, api } from '@api'; | ||||||
|  | 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; | ||||||
|  | 	import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let sharedLink: SharedLinkResponseDto; | ||||||
|  | 	export let allAssets: AssetResponseDto[]; | ||||||
|  | 
 | ||||||
|  | 	const { getAssets, clearSelect } = getAssetControlContext(); | ||||||
|  | 
 | ||||||
|  | 	const handleRemoveAssetsFromSharedLink = async () => { | ||||||
|  | 		if (window.confirm('Do you want to remove selected assets from the shared link?')) { | ||||||
|  | 			// TODO: Rename API method or change functionality. The assetIds passed | ||||||
|  | 			// in are kept instead of removed. | ||||||
|  | 			const assetsToKeep = allAssets.filter((a) => !getAssets().has(a)); | ||||||
|  | 			await api.assetApi.removeAssetsFromSharedLink( | ||||||
|  | 				{ | ||||||
|  | 					assetIds: assetsToKeep.map((a) => a.id) | ||||||
|  | 				}, | ||||||
|  | 				sharedLink?.key | ||||||
|  | 			); | ||||||
|  | 
 | ||||||
|  | 			sharedLink.assets = assetsToKeep; | ||||||
|  | 			clearSelect(); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <CircleIconButton | ||||||
|  | 	title="Remove from album" | ||||||
|  | 	on:click={handleRemoveAssetsFromSharedLink} | ||||||
|  | 	logo={DeleteOutline} | ||||||
|  | /> | ||||||
| @ -0,0 +1,35 @@ | |||||||
|  | <script lang="ts" context="module"> | ||||||
|  | 	import { createContext } from '$lib/utils/context'; | ||||||
|  | 
 | ||||||
|  | 	const { get: getMenuContext, set: setContext } = createContext<() => void>(); | ||||||
|  | 	export { getMenuContext }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||||
|  | 	import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'; | ||||||
|  | 	import type Icon from 'svelte-material-icons/AbTesting.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let icon: typeof Icon; | ||||||
|  | 	export let title: string; | ||||||
|  | 
 | ||||||
|  | 	let showContextMenu = false; | ||||||
|  | 	let contextMenuPosition = { x: 0, y: 0 }; | ||||||
|  | 
 | ||||||
|  | 	const handleShowMenu = ({ x, y }: MouseEvent) => { | ||||||
|  | 		contextMenuPosition = { x, y }; | ||||||
|  | 		showContextMenu = !showContextMenu; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	setContext(() => (showContextMenu = false)); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <CircleIconButton {title} logo={icon} on:click={handleShowMenu} /> | ||||||
|  | 
 | ||||||
|  | {#if showContextMenu} | ||||||
|  | 	<ContextMenu {...contextMenuPosition} on:clickoutside={() => (showContextMenu = false)}> | ||||||
|  | 		<div class="flex flex-col rounded-lg"> | ||||||
|  | 			<slot /> | ||||||
|  | 		</div> | ||||||
|  | 	</ContextMenu> | ||||||
|  | {/if} | ||||||
| @ -0,0 +1,39 @@ | |||||||
|  | <script lang="ts" context="module"> | ||||||
|  | 	import { createContext } from '$lib/utils/context'; | ||||||
|  | 
 | ||||||
|  | 	export type OnAssetDelete = (assetId: string) => void; | ||||||
|  | 	export type OnAssetArchive = (asset: AssetResponseDto, archived: boolean) => void; | ||||||
|  | 	export type OnAssetFavorite = (asset: AssetResponseDto, favorite: boolean) => void; | ||||||
|  | 
 | ||||||
|  | 	export interface AssetControlContext { | ||||||
|  | 		// Wrap assets in a function, because context isn't reactive. | ||||||
|  | 		getAssets: () => Set<AssetResponseDto>; | ||||||
|  | 		clearSelect: () => void; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const { get: getAssetControlContext, set: setContext } = createContext<AssetControlContext>(); | ||||||
|  | 	export { getAssetControlContext }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
|  | 	import { AssetResponseDto } from '@api'; | ||||||
|  | 	import Close from 'svelte-material-icons/Close.svelte'; | ||||||
|  | 	import ControlAppBar from '../shared-components/control-app-bar.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let assets: Set<AssetResponseDto>; | ||||||
|  | 	export let clearSelect: () => void; | ||||||
|  | 
 | ||||||
|  | 	setContext({ getAssets: () => assets, clearSelect }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <ControlAppBar | ||||||
|  | 	on:close-button-click={clearSelect} | ||||||
|  | 	backIcon={Close} | ||||||
|  | 	tailwindClasses="bg-white shadow-md" | ||||||
|  | > | ||||||
|  | 	<p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading"> | ||||||
|  | 		Selected {assets.size.toLocaleString($locale)} | ||||||
|  | 	</p> | ||||||
|  | 	<slot slot="trailing" /> | ||||||
|  | </ControlAppBar> | ||||||
| @ -0,0 +1,67 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import { goto } from '$app/navigation'; | ||||||
|  | 	import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte'; | ||||||
|  | 	import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; | ||||||
|  | 	import { | ||||||
|  | 		NotificationType, | ||||||
|  | 		notificationController | ||||||
|  | 	} from '$lib/components/shared-components/notification/notification'; | ||||||
|  | 	import { addAssetsToAlbum } from '$lib/utils/asset-utils'; | ||||||
|  | 	import { AlbumResponseDto, api } from '@api'; | ||||||
|  | 	import { getMenuContext } from '../asset-select-context-menu.svelte'; | ||||||
|  | 	import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let shared = false; | ||||||
|  | 	let showAlbumPicker = false; | ||||||
|  | 
 | ||||||
|  | 	const { getAssets, clearSelect } = getAssetControlContext(); | ||||||
|  | 	const closeMenu = getMenuContext(); | ||||||
|  | 
 | ||||||
|  | 	const handleHideAlbumPicker = () => { | ||||||
|  | 		showAlbumPicker = false; | ||||||
|  | 		closeMenu(); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const handleAddToNewAlbum = (event: CustomEvent) => { | ||||||
|  | 		showAlbumPicker = false; | ||||||
|  | 
 | ||||||
|  | 		const { albumName }: { albumName: string } = event.detail; | ||||||
|  | 		const assetIds = Array.from(getAssets()).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 | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			clearSelect(); | ||||||
|  | 
 | ||||||
|  | 			goto('/albums/' + id); | ||||||
|  | 		}); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const handleAddToAlbum = async (event: CustomEvent<{ album: AlbumResponseDto }>) => { | ||||||
|  | 		showAlbumPicker = false; | ||||||
|  | 		const album = event.detail.album; | ||||||
|  | 
 | ||||||
|  | 		const assetIds = Array.from(getAssets()).map((asset) => asset.id); | ||||||
|  | 
 | ||||||
|  | 		addAssetsToAlbum(album.id, assetIds).then(clearSelect); | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <MenuOption | ||||||
|  | 	on:click={() => (showAlbumPicker = true)} | ||||||
|  | 	text={shared ? 'Add to Shared Album' : 'Add to Album'} | ||||||
|  | /> | ||||||
|  | 
 | ||||||
|  | {#if showAlbumPicker} | ||||||
|  | 	<AlbumSelectionModal | ||||||
|  | 		{shared} | ||||||
|  | 		on:newAlbum={handleAddToNewAlbum} | ||||||
|  | 		on:newSharedAlbum={handleAddToNewAlbum} | ||||||
|  | 		on:album={handleAddToAlbum} | ||||||
|  | 		on:close={handleHideAlbumPicker} | ||||||
|  | 	/> | ||||||
|  | {/if} | ||||||
| @ -0,0 +1,41 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; | ||||||
|  | 	import { | ||||||
|  | 		NotificationType, | ||||||
|  | 		notificationController | ||||||
|  | 	} from '$lib/components/shared-components/notification/notification'; | ||||||
|  | 	import { api } from '@api'; | ||||||
|  | 	import { getMenuContext } from '../asset-select-context-menu.svelte'; | ||||||
|  | 	import { OnAssetFavorite, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let onAssetFavorite: OnAssetFavorite = (asset, favorite) => { | ||||||
|  | 		asset.isFavorite = favorite; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const { getAssets, clearSelect } = getAssetControlContext(); | ||||||
|  | 	const closeMenu = getMenuContext(); | ||||||
|  | 
 | ||||||
|  | 	const handleAddToFavorites = () => { | ||||||
|  | 		closeMenu(); | ||||||
|  | 
 | ||||||
|  | 		let cnt = 0; | ||||||
|  | 		for (const asset of getAssets()) { | ||||||
|  | 			if (!asset.isFavorite) { | ||||||
|  | 				api.assetApi.updateAsset(asset.id, { | ||||||
|  | 					isFavorite: true | ||||||
|  | 				}); | ||||||
|  | 				onAssetFavorite(asset, true); | ||||||
|  | 				cnt = cnt + 1; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		notificationController.show({ | ||||||
|  | 			message: `Added ${cnt} to favorites`, | ||||||
|  | 			type: NotificationType.Info | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		clearSelect(); | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <MenuOption on:click={handleAddToFavorites} text="Add to Favorites" /> | ||||||
| @ -0,0 +1,19 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  | 	import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; | ||||||
|  | 	import RemoveFavorite from '../actions/remove-favorite.svelte'; | ||||||
|  | 	import { getMenuContext } from '../asset-select-context-menu.svelte'; | ||||||
|  | 	import { OnAssetFavorite } from '../asset-select-control-bar.svelte'; | ||||||
|  | 
 | ||||||
|  | 	export let onAssetFavorite: OnAssetFavorite | undefined = undefined; | ||||||
|  | 	const closeMenu = getMenuContext(); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <RemoveFavorite let:handleRemoveFavorite {onAssetFavorite}> | ||||||
|  | 	<MenuOption | ||||||
|  | 		on:click={() => { | ||||||
|  | 			closeMenu(); | ||||||
|  | 			handleRemoveFavorite(); | ||||||
|  | 		}} | ||||||
|  | 		text="Remove from favorites" | ||||||
|  | 	/> | ||||||
|  | </RemoveFavorite> | ||||||
| @ -1,47 +1,37 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; |  | ||||||
| 
 |  | ||||||
| 	import { api, AssetResponseDto, SharedLinkResponseDto } from '@api'; |  | ||||||
| 	import ControlAppBar from '../shared-components/control-app-bar.svelte'; |  | ||||||
| 	import { goto } from '$app/navigation'; | 	import { goto } from '$app/navigation'; | ||||||
| 	import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | 	import { bulkDownload } from '$lib/utils/asset-utils'; | ||||||
|  | 	import { openFileUploadDialog } from '$lib/utils/file-uploader'; | ||||||
|  | 	import { api, AssetResponseDto, SharedLinkResponseDto } from '@api'; | ||||||
|  | 	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; | ||||||
| 	import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte'; | 	import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte'; | ||||||
| 	import FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte'; | 	import FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte'; | ||||||
| 	import { openFileUploadDialog } from '$lib/utils/file-uploader'; | 	import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | ||||||
| 	import { bulkDownload } from '$lib/utils/asset-utils'; | 	import DownloadFiles from '../photos-page/actions/download-files.svelte'; | ||||||
| 	import Close from 'svelte-material-icons/Close.svelte'; | 	import RemoveFromSharedLink from '../photos-page/actions/remove-from-shared-link.svelte'; | ||||||
| 	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte'; | 	import AssetSelectControlBar from '../photos-page/asset-select-control-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 DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; |  | ||||||
| 	import ImmichLogo from '../shared-components/immich-logo.svelte'; | 	import ImmichLogo from '../shared-components/immich-logo.svelte'; | ||||||
| 	import { | 	import { | ||||||
| 		notificationController, | 		notificationController, | ||||||
| 		NotificationType | 		NotificationType | ||||||
| 	} from '../shared-components/notification/notification'; | 	} from '../shared-components/notification/notification'; | ||||||
| 	import { locale } from '$lib/stores/preferences.store'; |  | ||||||
| 
 | 
 | ||||||
| 	export let sharedLink: SharedLinkResponseDto; | 	export let sharedLink: SharedLinkResponseDto; | ||||||
| 	export let isOwned: boolean; | 	export let isOwned: boolean; | ||||||
| 
 | 
 | ||||||
| 	let assets = sharedLink.assets; |  | ||||||
| 	let selectedAssets: Set<AssetResponseDto> = new Set(); | 	let selectedAssets: Set<AssetResponseDto> = new Set(); | ||||||
| 
 | 
 | ||||||
|  | 	$: assets = sharedLink.assets; | ||||||
| 	$: isMultiSelectionMode = selectedAssets.size > 0; | 	$: isMultiSelectionMode = selectedAssets.size > 0; | ||||||
| 
 | 
 | ||||||
| 	const clearMultiSelectAssetAssetHandler = () => { | 	const clearMultiSelectAssetAssetHandler = () => { | ||||||
| 		selectedAssets = new Set(); | 		selectedAssets = new Set(); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const downloadAssets = async (isAll: boolean) => { | 	const downloadAssets = async () => { | ||||||
| 		await bulkDownload( | 		await bulkDownload('immich-shared', assets, undefined, sharedLink?.key); | ||||||
| 			'immich-shared', |  | ||||||
| 			isAll ? assets : Array.from(selectedAssets), |  | ||||||
| 			() => { |  | ||||||
| 				isMultiSelectionMode = false; |  | ||||||
| 				clearMultiSelectAssetAssetHandler(); |  | ||||||
| 			}, |  | ||||||
| 			sharedLink?.key |  | ||||||
| 		); |  | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const handleUploadAssets = async () => { | 	const handleUploadAssets = async () => { | ||||||
| @ -65,49 +55,16 @@ | |||||||
| 			console.error('handleUploadAssets', e); | 			console.error('handleUploadAssets', e); | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 |  | ||||||
| 	const handleRemoveAssetsFromSharedLink = async () => { |  | ||||||
| 		if (window.confirm('Do you want to remove selected assets from the shared link?')) { |  | ||||||
| 			await api.assetApi.removeAssetsFromSharedLink( |  | ||||||
| 				{ |  | ||||||
| 					assetIds: assets.filter((a) => !selectedAssets.has(a)).map((a) => a.id) |  | ||||||
| 				}, |  | ||||||
| 				sharedLink?.key |  | ||||||
| 			); |  | ||||||
| 
 |  | ||||||
| 			assets = assets.filter((a) => !selectedAssets.has(a)); |  | ||||||
| 			clearMultiSelectAssetAssetHandler(); |  | ||||||
| 		} |  | ||||||
| 	}; |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <section class="bg-immich-bg dark:bg-immich-dark-bg"> | <section class="bg-immich-bg dark:bg-immich-dark-bg"> | ||||||
| 	{#if isMultiSelectionMode} | 	{#if isMultiSelectionMode} | ||||||
| 		<ControlAppBar | 		<AssetSelectControlBar assets={selectedAssets} clearSelect={clearMultiSelectAssetAssetHandler}> | ||||||
| 			on:close-button-click={clearMultiSelectAssetAssetHandler} | 			<DownloadFiles filename="immich-shared" sharedLinkKey={sharedLink.key} /> | ||||||
| 			backIcon={Close} | 			{#if isOwned} | ||||||
| 			tailwindClasses={'bg-white shadow-md'} | 				<RemoveFromSharedLink bind:sharedLink allAssets={assets} /> | ||||||
| 		> | 			{/if} | ||||||
| 			<svelte:fragment slot="leading"> | 		</AssetSelectControlBar> | ||||||
| 				<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="Download" |  | ||||||
| 					on:click={() => downloadAssets(false)} |  | ||||||
| 					logo={CloudDownloadOutline} |  | ||||||
| 				/> |  | ||||||
| 				{#if isOwned} |  | ||||||
| 					<CircleIconButton |  | ||||||
| 						title="Remove from album" |  | ||||||
| 						on:click={handleRemoveAssetsFromSharedLink} |  | ||||||
| 						logo={DeleteOutline} |  | ||||||
| 					/> |  | ||||||
| 				{/if} |  | ||||||
| 			</svelte:fragment> |  | ||||||
| 		</ControlAppBar> |  | ||||||
| 	{:else} | 	{:else} | ||||||
| 		<ControlAppBar | 		<ControlAppBar | ||||||
| 			on:close-button-click={() => goto('/photos')} | 			on:close-button-click={() => goto('/photos')} | ||||||
| @ -139,7 +96,7 @@ | |||||||
| 				{#if sharedLink?.allowDownload} | 				{#if sharedLink?.allowDownload} | ||||||
| 					<CircleIconButton | 					<CircleIconButton | ||||||
| 						title="Download" | 						title="Download" | ||||||
| 						on:click={() => downloadAssets(true)} | 						on:click={downloadAssets} | ||||||
| 						logo={FolderDownloadOutline} | 						logo={FolderDownloadOutline} | ||||||
| 					/> | 					/> | ||||||
| 				{/if} | 				{/if} | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ export const addAssetsToAlbum = async ( | |||||||
| export async function bulkDownload( | export async function bulkDownload( | ||||||
| 	fileName: string, | 	fileName: string, | ||||||
| 	assets: AssetResponseDto[], | 	assets: AssetResponseDto[], | ||||||
| 	onDone: () => void, | 	onDone?: () => void, | ||||||
| 	key?: string | 	key?: string | ||||||
| ) { | ) { | ||||||
| 	const assetIds = assets.map((asset) => asset.id); | 	const assetIds = assets.map((asset) => asset.id); | ||||||
| @ -63,7 +63,7 @@ export async function bulkDownload( | |||||||
| 			if (isNotComplete && fileCount > 0) { | 			if (isNotComplete && fileCount > 0) { | ||||||
| 				// skip += fileCount;
 | 				// skip += fileCount;
 | ||||||
| 			} else { | 			} else { | ||||||
| 				onDone(); | 				onDone?.(); | ||||||
| 				done = true; | 				done = true; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,34 +1,27 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { 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 ControlAppBar from '$lib/components/shared-components/control-app-bar.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 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'; |  | ||||||
| 	import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; |  | ||||||
| 	import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; | 	import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; | ||||||
| 	import type { PageData } from './$types'; | 	import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; | ||||||
| 	import { onMount } from 'svelte'; | 	import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; | ||||||
| 	import { handleError } from '$lib/utils/handle-error'; | 	import DownloadFiles from '$lib/components/photos-page/actions/download-files.svelte'; | ||||||
|  | 	import RemoveFromArchive from '$lib/components/photos-page/actions/remove-from-archive.svelte'; | ||||||
|  | 	import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; | ||||||
|  | 	import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; | ||||||
|  | 	import OptionAddToAlbum from '$lib/components/photos-page/menu-options/option-add-to-album.svelte'; | ||||||
|  | 	import OptionAddToFavorites from '$lib/components/photos-page/menu-options/option-add-to-favorites.svelte'; | ||||||
|  | 	import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.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 { archivedAsset } from '$lib/stores/archived-asset.store'; | 	import { archivedAsset } from '$lib/stores/archived-asset.store'; | ||||||
|  | 	import { handleError } from '$lib/utils/handle-error'; | ||||||
|  | 	import { api, AssetResponseDto } from '@api'; | ||||||
|  | 	import { onMount } from 'svelte'; | ||||||
|  | 	import Plus from 'svelte-material-icons/Plus.svelte'; | ||||||
|  | 	import type { PageData } from './$types'; | ||||||
| 
 | 
 | ||||||
| 	export let data: PageData; | 	export let data: PageData; | ||||||
| 
 | 
 | ||||||
|  | 	let selectedAssets: Set<AssetResponseDto> = new Set(); | ||||||
|  | 	$: isMultiSelectionMode = selectedAssets.size > 0; | ||||||
|  | 
 | ||||||
| 	onMount(async () => { | 	onMount(async () => { | ||||||
| 		try { | 		try { | ||||||
| 			const { data: assets } = await api.assetApi.getAllAssets(undefined, true); | 			const { data: assets } = await api.assetApi.getAllAssets(undefined, true); | ||||||
| @ -38,144 +31,8 @@ | |||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	const clearMultiSelectAssetAssetHandler = () => { | 	const onAssetDelete = (assetId: string) => { | ||||||
| 		selectedAssets = new Set(); | 		$archivedAsset = $archivedAsset.filter((a) => a.id !== assetId); | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	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') { |  | ||||||
| 						$archivedAsset = $archivedAsset.filter((a) => 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); |  | ||||||
| 		} |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	$: isMultiSelectionMode = selectedAssets.size > 0; |  | ||||||
| 
 |  | ||||||
| 	let selectedAssets: Set<AssetResponseDto> = new Set(); |  | ||||||
| 
 |  | ||||||
| 	let contextMenuPosition = { x: 0, y: 0 }; |  | ||||||
| 	let isShowCreateSharedLinkModal = false; |  | ||||||
| 	let isShowAddMenu = false; |  | ||||||
| 	let isShowAlbumPicker = false; |  | ||||||
| 	let addToSharedAlbum = false; |  | ||||||
| 
 |  | ||||||
| 	const handleShowMenu = ({ x, y }: MouseEvent) => { |  | ||||||
| 		contextMenuPosition = { x, y }; |  | ||||||
| 		isShowAddMenu = !isShowAddMenu; |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const handleAddToFavorites = () => { |  | ||||||
| 		isShowAddMenu = false; |  | ||||||
| 
 |  | ||||||
| 		let cnt = 0; |  | ||||||
| 		for (const asset of selectedAssets) { |  | ||||||
| 			if (!asset.isFavorite) { |  | ||||||
| 				api.assetApi.updateAsset(asset.id, { |  | ||||||
| 					isFavorite: true |  | ||||||
| 				}); |  | ||||||
| 				cnt = cnt + 1; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		notificationController.show({ |  | ||||||
| 			message: `Added ${cnt} to favorites`, |  | ||||||
| 			type: NotificationType.Info |  | ||||||
| 		}); |  | ||||||
| 
 |  | ||||||
| 		clearMultiSelectAssetAssetHandler(); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	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 handleUnarchive = async () => { |  | ||||||
| 		let cnt = 0; |  | ||||||
| 		for (const asset of selectedAssets) { |  | ||||||
| 			if (asset.isArchived) { |  | ||||||
| 				api.assetApi.updateAsset(asset.id, { |  | ||||||
| 					isArchived: false |  | ||||||
| 				}); |  | ||||||
| 				cnt = cnt + 1; |  | ||||||
| 
 |  | ||||||
| 				$archivedAsset = $archivedAsset.filter((a) => a.id != asset.id); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		notificationController.show({ |  | ||||||
| 			message: `Removed ${cnt} from archive`, |  | ||||||
| 			type: NotificationType.Info |  | ||||||
| 		}); |  | ||||||
| 
 |  | ||||||
| 		clearMultiSelectAssetAssetHandler(); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const handleCreateSharedLink = async () => { |  | ||||||
| 		isShowCreateSharedLinkModal = true; |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const handleCloseSharedLinkModal = () => { |  | ||||||
| 		clearMultiSelectAssetAssetHandler(); |  | ||||||
| 		isShowCreateSharedLinkModal = false; |  | ||||||
| 	}; | 	}; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| @ -190,68 +47,20 @@ | |||||||
| 
 | 
 | ||||||
| 	<svelte:fragment slot="header"> | 	<svelte:fragment slot="header"> | ||||||
| 		{#if isMultiSelectionMode} | 		{#if isMultiSelectionMode} | ||||||
| 			<ControlAppBar | 			<AssetSelectControlBar | ||||||
| 				on:close-button-click={clearMultiSelectAssetAssetHandler} | 				assets={selectedAssets} | ||||||
| 				backIcon={Close} | 				clearSelect={() => (selectedAssets = new Set())} | ||||||
| 				tailwindClasses={'bg-white shadow-md'} |  | ||||||
| 			> | 			> | ||||||
| 				<svelte:fragment slot="leading"> | 				<CreateSharedLink /> | ||||||
| 					<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | 				<RemoveFromArchive onAssetArchive={(asset) => onAssetDelete(asset.id)} /> | ||||||
| 						Selected {selectedAssets.size.toLocaleString($locale)} | 				<DownloadFiles /> | ||||||
| 					</p> | 				<AssetSelectContextMenu icon={Plus} title="Add"> | ||||||
| 				</svelte:fragment> | 					<OptionAddToFavorites /> | ||||||
| 				<svelte:fragment slot="trailing"> | 					<OptionAddToAlbum /> | ||||||
| 					<CircleIconButton | 					<OptionAddToAlbum shared /> | ||||||
| 						title="Share" | 				</AssetSelectContextMenu> | ||||||
| 						logo={ShareVariantOutline} | 				<DeleteAssets {onAssetDelete} /> | ||||||
| 						on:click={handleCreateSharedLink} | 			</AssetSelectControlBar> | ||||||
| 					/> |  | ||||||
| 					<CircleIconButton |  | ||||||
| 						title="Unarchive" |  | ||||||
| 						logo={ArchiveArrowUpOutline} |  | ||||||
| 						on:click={handleUnarchive} |  | ||||||
| 					/> |  | ||||||
| 					<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> |  | ||||||
| 		{/if} |  | ||||||
| 
 |  | ||||||
| 		{#if isShowAddMenu} |  | ||||||
| 			<ContextMenu {...contextMenuPosition} on:clickoutside={() => (isShowAddMenu = false)}> |  | ||||||
| 				<div class="flex flex-col rounded-lg"> |  | ||||||
| 					<MenuOption on:click={handleAddToFavorites} text="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} | 		{/if} | ||||||
| 	</svelte:fragment> | 	</svelte:fragment> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,21 +1,17 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | 	import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; | ||||||
| 	import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; | 	import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; | ||||||
| 	import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'; | 	import RemoveFavorite from '$lib/components/photos-page/actions/remove-favorite.svelte'; | ||||||
|  | 	import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; | ||||||
|  | 	import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.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 { handleError } from '$lib/utils/handle-error'; | 	import { handleError } from '$lib/utils/handle-error'; | ||||||
| 	import { api, AssetResponseDto, SharedLinkType } from '@api'; | 	import { api, AssetResponseDto } from '@api'; | ||||||
| 	import { onMount } from 'svelte'; | 	import { onMount } from 'svelte'; | ||||||
| 	import Close from 'svelte-material-icons/Close.svelte'; |  | ||||||
| 	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; |  | ||||||
| 	import HeartMinusOutline from 'svelte-material-icons/HeartMinusOutline.svelte'; |  | ||||||
| 	import Error from '../../+error.svelte'; | 	import Error from '../../+error.svelte'; | ||||||
| 	import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; |  | ||||||
| 	import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; |  | ||||||
| 	import type { PageData } from './$types'; | 	import type { PageData } from './$types'; | ||||||
| 
 | 
 | ||||||
| 	let favorites: AssetResponseDto[] = []; | 	let favorites: AssetResponseDto[] = []; | ||||||
| 	let isShowCreateSharedLinkModal = false; |  | ||||||
| 	let selectedAssets: Set<AssetResponseDto> = new Set(); | 	let selectedAssets: Set<AssetResponseDto> = new Set(); | ||||||
| 
 | 
 | ||||||
| 	export let data: PageData; | 	export let data: PageData; | ||||||
| @ -31,69 +27,17 @@ | |||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	const clearMultiSelectAssetAssetHandler = () => { | 	const onAssetDelete = (assetId: string) => { | ||||||
| 		selectedAssets = new Set(); | 		favorites = favorites.filter((a) => a.id !== assetId); | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const handleCreateSharedLink = async () => { |  | ||||||
| 		isShowCreateSharedLinkModal = true; |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const handleCloseSharedLinkModal = () => { |  | ||||||
| 		clearMultiSelectAssetAssetHandler(); |  | ||||||
| 		isShowCreateSharedLinkModal = false; |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const handleRemoveFavorite = async () => { |  | ||||||
| 		for (const asset of selectedAssets) { |  | ||||||
| 			try { |  | ||||||
| 				await api.assetApi.updateAsset(asset.id, { |  | ||||||
| 					isFavorite: false |  | ||||||
| 				}); |  | ||||||
| 				favorites = favorites.filter((a) => a.id != asset.id); |  | ||||||
| 			} catch { |  | ||||||
| 				handleError(Error, 'Error updating asset favorite state'); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		clearMultiSelectAssetAssetHandler(); |  | ||||||
| 	}; | 	}; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <!-- Multiselection mode app bar --> | <!-- Multiselection mode app bar --> | ||||||
| {#if isMultiSelectionMode} | {#if isMultiSelectionMode} | ||||||
| 	<ControlAppBar | 	<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}> | ||||||
| 		on:close-button-click={clearMultiSelectAssetAssetHandler} | 		<CreateSharedLink /> | ||||||
| 		backIcon={Close} | 		<RemoveFavorite onAssetFavorite={(asset) => onAssetDelete(asset.id)} /> | ||||||
| 		tailwindClasses={'bg-white shadow-md'} | 	</AssetSelectControlBar> | ||||||
| 	> |  | ||||||
| 		<svelte:fragment slot="leading"> |  | ||||||
| 			<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> |  | ||||||
| 				Selected {selectedAssets.size} |  | ||||||
| 			</p> |  | ||||||
| 		</svelte:fragment> |  | ||||||
| 		<svelte:fragment slot="trailing"> |  | ||||||
| 			<CircleIconButton |  | ||||||
| 				title="Share" |  | ||||||
| 				logo={ShareVariantOutline} |  | ||||||
| 				on:click={handleCreateSharedLink} |  | ||||||
| 			/> |  | ||||||
| 			<CircleIconButton |  | ||||||
| 				title="Remove Favorite" |  | ||||||
| 				logo={HeartMinusOutline} |  | ||||||
| 				on:click={handleRemoveFavorite} |  | ||||||
| 			/> |  | ||||||
| 		</svelte:fragment> |  | ||||||
| 	</ControlAppBar> |  | ||||||
| {/if} |  | ||||||
| 
 |  | ||||||
| <!-- Create shared link modal --> |  | ||||||
| {#if isShowCreateSharedLinkModal} |  | ||||||
| 	<CreateSharedLinkModal |  | ||||||
| 		sharedAssets={Array.from(selectedAssets)} |  | ||||||
| 		shareType={SharedLinkType.Individual} |  | ||||||
| 		on:close={handleCloseSharedLinkModal} |  | ||||||
| 	/> |  | ||||||
| {/if} | {/if} | ||||||
| 
 | 
 | ||||||
| <UserPageLayout user={data.user} hideNavbar={isMultiSelectionMode}> | <UserPageLayout user={data.user} hideNavbar={isMultiSelectionMode}> | ||||||
|  | |||||||
| @ -1,52 +1,34 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import type { PageData } from './$types'; |  | ||||||
| 	import { AppRoute } from '$lib/constants'; |  | ||||||
| 	import { locale } from '$lib/stores/preferences.store'; |  | ||||||
| 	import { goto } from '$app/navigation'; | 	import { goto } from '$app/navigation'; | ||||||
| 	import { bulkDownload } from '$lib/utils/asset-utils'; | 	import DownloadFiles from '$lib/components/photos-page/actions/download-files.svelte'; | ||||||
| 	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; |  | ||||||
| 	import Close from 'svelte-material-icons/Close.svelte'; |  | ||||||
| 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; |  | ||||||
| 	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte'; |  | ||||||
| 	import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; |  | ||||||
| 	import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; | 	import AssetGrid from '$lib/components/photos-page/asset-grid.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 { AppRoute } from '$lib/constants'; | ||||||
| 	import { | 	import { | ||||||
| 		assetInteractionStore, | 		assetInteractionStore, | ||||||
| 		isMultiSelectStoreState, | 		isMultiSelectStoreState, | ||||||
| 		selectedAssets | 		selectedAssets | ||||||
| 	} from '$lib/stores/asset-interaction.store'; | 	} from '$lib/stores/asset-interaction.store'; | ||||||
|  | 	import { onDestroy } from 'svelte'; | ||||||
|  | 	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; | ||||||
|  | 	import type { PageData } from './$types'; | ||||||
| 
 | 
 | ||||||
| 	export let data: PageData; | 	export let data: PageData; | ||||||
| 
 | 
 | ||||||
| 	const handleDownloadFiles = async () => { | 	onDestroy(() => { | ||||||
| 		await bulkDownload('immich', Array.from($selectedAssets), () => { | 		assetInteractionStore.clearMultiselect(); | ||||||
| 			assetInteractionStore.clearMultiselect(); | 	}); | ||||||
| 		}); |  | ||||||
| 	}; |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <main class="grid h-screen pt-[4.25rem] bg-immich-bg dark:bg-immich-dark-bg"> | <main class="grid h-screen pt-[4.25rem] bg-immich-bg dark:bg-immich-dark-bg"> | ||||||
| 	{#if $isMultiSelectStoreState} | 	{#if $isMultiSelectStoreState} | ||||||
| 		<ControlAppBar | 		<AssetSelectControlBar | ||||||
| 			showBackButton | 			assets={$selectedAssets} | ||||||
| 			backIcon={Close} | 			clearSelect={assetInteractionStore.clearMultiselect} | ||||||
| 			on:close-button-click={() => assetInteractionStore.clearMultiselect()} |  | ||||||
| 			tailwindClasses={'bg-white shadow-md'} |  | ||||||
| 		> | 		> | ||||||
| 			<svelte:fragment slot="leading"> | 			<DownloadFiles /> | ||||||
| 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | 		</AssetSelectControlBar> | ||||||
| 					Selected {$selectedAssets.size.toLocaleString($locale)} |  | ||||||
| 				</p> |  | ||||||
| 			</svelte:fragment> |  | ||||||
| 
 |  | ||||||
| 			<svelte:fragment slot="trailing"> |  | ||||||
| 				<CircleIconButton |  | ||||||
| 					title="Download" |  | ||||||
| 					logo={CloudDownloadOutline} |  | ||||||
| 					on:click={handleDownloadFiles} |  | ||||||
| 				/> |  | ||||||
| 			</svelte:fragment> |  | ||||||
| 		</ControlAppBar> |  | ||||||
| 	{:else} | 	{:else} | ||||||
| 		<ControlAppBar | 		<ControlAppBar | ||||||
| 			showBackButton | 			showBackButton | ||||||
|  | |||||||
| @ -1,235 +1,48 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { goto } from '$app/navigation'; | 	import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; | ||||||
|  | 	import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; | ||||||
|  | 	import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; | ||||||
|  | 	import DownloadFiles from '$lib/components/photos-page/actions/download-files.svelte'; | ||||||
|  | 	import MoveToArchive from '$lib/components/photos-page/actions/move-to-archive.svelte'; | ||||||
| 	import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; | 	import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; | ||||||
| 	import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte'; | 	import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; | ||||||
| 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | 	import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; | ||||||
| 	import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'; | 	import OptionAddToAlbum from '$lib/components/photos-page/menu-options/option-add-to-album.svelte'; | ||||||
| 	import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; | 	import OptionAddToFavorites from '$lib/components/photos-page/menu-options/option-add-to-favorites.svelte'; | ||||||
| 	import ControlAppBar from '$lib/components/shared-components/control-app-bar.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 { | 	import { | ||||||
| 		assetInteractionStore, | 		assetInteractionStore, | ||||||
| 		isMultiSelectStoreState, | 		isMultiSelectStoreState, | ||||||
| 		selectedAssets | 		selectedAssets | ||||||
| 	} from '$lib/stores/asset-interaction.store'; | 	} from '$lib/stores/asset-interaction.store'; | ||||||
| 	import { assetStore } from '$lib/stores/assets.store'; | 	import { assetStore } from '$lib/stores/assets.store'; | ||||||
| 	import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils'; | 	import { onDestroy } from 'svelte'; | ||||||
| 	import { AlbumResponseDto, api, SharedLinkType } from '@api'; |  | ||||||
| 	import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte'; |  | ||||||
| 	import Close from 'svelte-material-icons/Close.svelte'; |  | ||||||
| 	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte'; |  | ||||||
| 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; |  | ||||||
| 	import Plus from 'svelte-material-icons/Plus.svelte'; | 	import Plus from 'svelte-material-icons/Plus.svelte'; | ||||||
| 	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; |  | ||||||
| 	import { locale } from '$lib/stores/preferences.store'; |  | ||||||
| 	import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; |  | ||||||
| 	import type { PageData } from './$types'; | 	import type { PageData } from './$types'; | ||||||
| 
 | 
 | ||||||
| 	export let data: PageData; | 	export let data: PageData; | ||||||
| 
 | 
 | ||||||
| 	let isShowCreateSharedLinkModal = false; | 	onDestroy(() => { | ||||||
| 	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') { |  | ||||||
| 						assetStore.removeAsset(asset.id); |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				assetInteractionStore.clearMultiselect(); |  | ||||||
| 			} |  | ||||||
| 		} catch (e) { |  | ||||||
| 			notificationController.show({ |  | ||||||
| 				type: NotificationType.Error, |  | ||||||
| 				message: 'Error deleting assets, check console for more details' |  | ||||||
| 			}); |  | ||||||
| 			console.error('Error deleteSelectedAssetHandler', e); |  | ||||||
| 		} |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	let contextMenuPosition = { x: 0, y: 0 }; |  | ||||||
| 	let isShowAddMenu = false; |  | ||||||
| 	let isShowAlbumPicker = false; |  | ||||||
| 	let addToSharedAlbum = false; |  | ||||||
| 
 |  | ||||||
| 	const handleShowMenu = ({ x, y }: MouseEvent) => { |  | ||||||
| 		contextMenuPosition = { x, y }; |  | ||||||
| 		isShowAddMenu = !isShowAddMenu; |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const handleArchive = async () => { |  | ||||||
| 		let cnt = 0; |  | ||||||
| 		for (const asset of $selectedAssets) { |  | ||||||
| 			if (!asset.isArchived) { |  | ||||||
| 				api.assetApi.updateAsset(asset.id, { |  | ||||||
| 					isArchived: true |  | ||||||
| 				}); |  | ||||||
| 
 |  | ||||||
| 				assetStore.removeAsset(asset.id); |  | ||||||
| 				cnt = cnt + 1; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		notificationController.show({ |  | ||||||
| 			message: `Archived ${cnt}`, |  | ||||||
| 			type: NotificationType.Info |  | ||||||
| 		}); |  | ||||||
| 
 |  | ||||||
| 		assetInteractionStore.clearMultiselect(); | 		assetInteractionStore.clearMultiselect(); | ||||||
| 	}; | 	}); | ||||||
| 
 |  | ||||||
| 	const handleAddToFavorites = () => { |  | ||||||
| 		isShowAddMenu = false; |  | ||||||
| 
 |  | ||||||
| 		let cnt = 0; |  | ||||||
| 		for (const asset of $selectedAssets) { |  | ||||||
| 			if (!asset.isFavorite) { |  | ||||||
| 				api.assetApi.updateAsset(asset.id, { |  | ||||||
| 					isFavorite: true |  | ||||||
| 				}); |  | ||||||
| 				assetStore.updateAsset(asset.id, true); |  | ||||||
| 				cnt = cnt + 1; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		notificationController.show({ |  | ||||||
| 			message: `Added ${cnt} to favorites`, |  | ||||||
| 			type: NotificationType.Info |  | ||||||
| 		}); |  | ||||||
| 
 |  | ||||||
| 		assetInteractionStore.clearMultiselect(); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	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 |  | ||||||
| 			}); |  | ||||||
| 
 |  | ||||||
| 			assetInteractionStore.clearMultiselect(); |  | ||||||
| 
 |  | ||||||
| 			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(() => { |  | ||||||
| 			assetInteractionStore.clearMultiselect(); |  | ||||||
| 		}); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const handleDownloadFiles = async () => { |  | ||||||
| 		await bulkDownload('immich', Array.from($selectedAssets), () => { |  | ||||||
| 			assetInteractionStore.clearMultiselect(); |  | ||||||
| 		}); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const handleCreateSharedLink = async () => { |  | ||||||
| 		isShowCreateSharedLinkModal = true; |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	const handleCloseSharedLinkModal = () => { |  | ||||||
| 		assetInteractionStore.clearMultiselect(); |  | ||||||
| 		isShowCreateSharedLinkModal = false; |  | ||||||
| 	}; |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <UserPageLayout user={data.user} hideNavbar={$isMultiSelectStoreState} showUploadButton> | <UserPageLayout user={data.user} hideNavbar={$isMultiSelectStoreState} showUploadButton> | ||||||
| 	<svelte:fragment slot="header"> | 	<svelte:fragment slot="header"> | ||||||
| 		{#if $isMultiSelectStoreState} | 		{#if $isMultiSelectStoreState} | ||||||
| 			<ControlAppBar | 			<AssetSelectControlBar | ||||||
| 				on:close-button-click={() => assetInteractionStore.clearMultiselect()} | 				assets={$selectedAssets} | ||||||
| 				backIcon={Close} | 				clearSelect={assetInteractionStore.clearMultiselect} | ||||||
| 				tailwindClasses={'bg-white shadow-md'} |  | ||||||
| 			> | 			> | ||||||
| 				<svelte:fragment slot="leading"> | 				<CreateSharedLink /> | ||||||
| 					<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | 				<MoveToArchive onAssetArchive={(asset) => assetStore.removeAsset(asset.id)} /> | ||||||
| 						Selected {$selectedAssets.size.toLocaleString($locale)} | 				<DownloadFiles /> | ||||||
| 					</p> | 				<AssetSelectContextMenu icon={Plus} title="Add"> | ||||||
| 				</svelte:fragment> | 					<OptionAddToFavorites /> | ||||||
| 				<svelte:fragment slot="trailing"> | 					<OptionAddToAlbum /> | ||||||
| 					<CircleIconButton | 					<OptionAddToAlbum shared /> | ||||||
| 						title="Share" | 				</AssetSelectContextMenu> | ||||||
| 						logo={ShareVariantOutline} | 				<DeleteAssets onAssetDelete={assetStore.removeAsset} /> | ||||||
| 						on:click={handleCreateSharedLink} | 			</AssetSelectControlBar> | ||||||
| 					/> |  | ||||||
| 					<CircleIconButton |  | ||||||
| 						title="Archive" |  | ||||||
| 						logo={ArchiveArrowDownOutline} |  | ||||||
| 						on:click={handleArchive} |  | ||||||
| 					/> |  | ||||||
| 					<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> |  | ||||||
| 		{/if} |  | ||||||
| 
 |  | ||||||
| 		{#if isShowAddMenu} |  | ||||||
| 			<ContextMenu {...contextMenuPosition} on:clickoutside={() => (isShowAddMenu = false)}> |  | ||||||
| 				<div class="flex flex-col rounded-lg"> |  | ||||||
| 					<MenuOption on:click={handleAddToFavorites} text="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} | 		{/if} | ||||||
| 	</svelte:fragment> | 	</svelte:fragment> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,31 +1,25 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | 	import { afterNavigate, goto } from '$app/navigation'; | ||||||
| 	import { page } from '$app/stores'; | 	import { page } from '$app/stores'; | ||||||
|  | 	import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; | ||||||
|  | 	import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; | ||||||
|  | 	import DownloadFiles from '$lib/components/photos-page/actions/download-files.svelte'; | ||||||
|  | 	import MoveToArchive from '$lib/components/photos-page/actions/move-to-archive.svelte'; | ||||||
|  | 	import RemoveFromArchive from '$lib/components/photos-page/actions/remove-from-archive.svelte'; | ||||||
|  | 	import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; | ||||||
|  | 	import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; | ||||||
|  | 	import OptionAddToAlbum from '$lib/components/photos-page/menu-options/option-add-to-album.svelte'; | ||||||
|  | 	import OptionAddToFavorites from '$lib/components/photos-page/menu-options/option-add-to-favorites.svelte'; | ||||||
|  | 	import OptionRemoveFromFavorites from '$lib/components/photos-page/menu-options/option-remove-from-favorites.svelte'; | ||||||
| 	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 type { PageData } from './$types'; | 	import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; | ||||||
|  | 	import { AssetResponseDto } from '@api'; | ||||||
| 	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; | 	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; | ||||||
| 	import ImageOffOutline from 'svelte-material-icons/ImageOffOutline.svelte'; | 	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 Plus from 'svelte-material-icons/Plus.svelte'; | ||||||
| 	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; | 	import type { PageData } from './$types'; | ||||||
| 	import { locale } from '$lib/stores/preferences.store'; | 
 | ||||||
| 	export let data: PageData; | 	export let data: PageData; | ||||||
| 
 | 
 | ||||||
| 	// The GalleryViewer pushes it's own history state, which causes weird | 	// The GalleryViewer pushes it's own history state, which causes weird | ||||||
| @ -46,197 +40,34 @@ | |||||||
| 	$: isMultiSelectionMode = selectedAssets.size > 0; | 	$: isMultiSelectionMode = selectedAssets.size > 0; | ||||||
| 	$: isAllArchived = Array.from(selectedAssets).every((asset) => asset.isArchived); | 	$: isAllArchived = Array.from(selectedAssets).every((asset) => asset.isArchived); | ||||||
| 	$: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite); | 	$: 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; | 	$: searchResultAssets = data.results.assets.items; | ||||||
| 
 | 
 | ||||||
| 	const handleShowMenu = ({ x, y }: MouseEvent) => { | 	const onAssetDelete = (assetId: string) => { | ||||||
| 		contextMenuPosition = { x, y }; | 		searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => a.id !== assetId); | ||||||
| 		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> | </script> | ||||||
| 
 | 
 | ||||||
| <section> | <section> | ||||||
| 	{#if isMultiSelectionMode} | 	{#if isMultiSelectionMode} | ||||||
| 		<ControlAppBar | 		<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}> | ||||||
| 			on:close-button-click={clearMultiSelectAssetAssetHandler} | 			<CreateSharedLink /> | ||||||
| 			backIcon={Close} | 			{#if isAllArchived} | ||||||
| 			tailwindClasses={'bg-white shadow-md'} | 				<RemoveFromArchive /> | ||||||
| 		> | 			{:else} | ||||||
| 			<svelte:fragment slot="leading"> | 				<MoveToArchive /> | ||||||
| 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | 			{/if} | ||||||
| 					Selected {selectedAssets.size.toLocaleString($locale)} | 			<DownloadFiles /> | ||||||
| 				</p> | 			<AssetSelectContextMenu icon={Plus} title="Add"> | ||||||
| 			</svelte:fragment> | 				{#if isAllFavorite} | ||||||
| 			<svelte:fragment slot="trailing"> | 					<OptionRemoveFromFavorites /> | ||||||
| 				<CircleIconButton | 				{:else} | ||||||
| 					title="Share" | 					<OptionAddToFavorites /> | ||||||
| 					logo={ShareVariantOutline} | 				{/if} | ||||||
| 					on:click={handleCreateSharedLink} | 				<OptionAddToAlbum /> | ||||||
| 				/> | 				<OptionAddToAlbum shared /> | ||||||
| 
 | 			</AssetSelectContextMenu> | ||||||
| 				<CircleIconButton | 			<DeleteAssets {onAssetDelete} /> | ||||||
| 					title={isAllArchived ? 'Unarchive' : 'Archive'} | 		</AssetSelectControlBar> | ||||||
| 					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} | 	{:else} | ||||||
| 		<ControlAppBar on:close-button-click={() => goto(previousRoute)} backIcon={ArrowLeft}> | 		<ControlAppBar on:close-button-click={() => goto(previousRoute)} backIcon={ArrowLeft}> | ||||||
| 			<div class="w-full max-w-2xl flex-1 pl-4"> | 			<div class="w-full max-w-2xl flex-1 pl-4"> | ||||||
| @ -271,35 +102,4 @@ | |||||||
| 			{/if} | 			{/if} | ||||||
| 		</section> | 		</section> | ||||||
| 	</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> | </section> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user