mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00: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:
		| @@ -1,49 +1,48 @@ | ||||
| <script lang="ts"> | ||||
| 	import { browser } from '$app/environment'; | ||||
| 	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 { | ||||
| 		AlbumResponseDto, | ||||
| 		api, | ||||
| 		AssetResponseDto, | ||||
| 		SharedLinkResponseDto, | ||||
| 		SharedLinkType, | ||||
| 		UserResponseDto | ||||
| 		UserResponseDto, | ||||
| 		api | ||||
| 	} from '@api'; | ||||
| 	import { onMount } from '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 FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte'; | ||||
| 	import { downloadAssets } from '$lib/stores/download'; | ||||
| 	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 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 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 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 ImmichLogo from '../shared-components/immich-logo.svelte'; | ||||
| 	import Button from '../elements/buttons/button.svelte'; | ||||
| 	import { clickOutside } from '$lib/utils/click-outside'; | ||||
| 	import { | ||||
| 		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 sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||
| @@ -125,25 +124,6 @@ | ||||
| 		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 | ||||
| 	$: { | ||||
| 		if (!isEditingTitle && currentAlbumName != album.albumName && isOwned) { | ||||
| @@ -353,48 +333,20 @@ | ||||
| 		isShowShareUserSelection = false; | ||||
| 		isShowShareLinkModal = true; | ||||
| 	}; | ||||
|  | ||||
| 	const handleDownloadSelectedAssets = async () => { | ||||
| 		await bulkDownload( | ||||
| 			album.albumName, | ||||
| 			Array.from(multiSelectAsset), | ||||
| 			() => { | ||||
| 				isMultiSelectionMode = false; | ||||
| 				clearMultiSelectAssetAssetHandler(); | ||||
| 			}, | ||||
| 			sharedLink?.key | ||||
| 		); | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <section class="bg-immich-bg dark:bg-immich-dark-bg" class:hidden={isShowThumbnailSelection}> | ||||
| 	<!-- Multiselection mode app bar --> | ||||
| 	{#if isMultiSelectionMode} | ||||
| 		<ControlAppBar | ||||
| 			on:close-button-click={clearMultiSelectAssetAssetHandler} | ||||
| 			backIcon={Close} | ||||
| 			tailwindClasses={'bg-white shadow-md'} | ||||
| 		<AssetSelectControlBar | ||||
| 			assets={multiSelectAsset} | ||||
| 			clearSelect={clearMultiSelectAssetAssetHandler} | ||||
| 		> | ||||
| 			<svelte:fragment slot="leading"> | ||||
| 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | ||||
| 					Selected {multiSelectAsset.size.toLocaleString($locale)} | ||||
| 				</p> | ||||
| 			</svelte:fragment> | ||||
| 			<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> | ||||
| 			<DownloadFiles filename={album.albumName} sharedLinkKey={sharedLink?.key} /> | ||||
| 			{#if isOwned} | ||||
| 				<RemoveFromAlbum bind:album /> | ||||
| 			{/if} | ||||
| 		</AssetSelectControlBar> | ||||
| 	{/if} | ||||
|  | ||||
| 	<!-- 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"> | ||||
| 	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 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 FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte'; | ||||
| 	import { openFileUploadDialog } from '$lib/utils/file-uploader'; | ||||
| 	import { bulkDownload } from '$lib/utils/asset-utils'; | ||||
| 	import Close from 'svelte-material-icons/Close.svelte'; | ||||
| 	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte'; | ||||
| 	import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | ||||
| 	import DownloadFiles from '../photos-page/actions/download-files.svelte'; | ||||
| 	import RemoveFromSharedLink from '../photos-page/actions/remove-from-shared-link.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 DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; | ||||
| 	import ImmichLogo from '../shared-components/immich-logo.svelte'; | ||||
| 	import { | ||||
| 		notificationController, | ||||
| 		NotificationType | ||||
| 	} from '../shared-components/notification/notification'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
|  | ||||
| 	export let sharedLink: SharedLinkResponseDto; | ||||
| 	export let isOwned: boolean; | ||||
|  | ||||
| 	let assets = sharedLink.assets; | ||||
| 	let selectedAssets: Set<AssetResponseDto> = new Set(); | ||||
|  | ||||
| 	$: assets = sharedLink.assets; | ||||
| 	$: isMultiSelectionMode = selectedAssets.size > 0; | ||||
|  | ||||
| 	const clearMultiSelectAssetAssetHandler = () => { | ||||
| 		selectedAssets = new Set(); | ||||
| 	}; | ||||
|  | ||||
| 	const downloadAssets = async (isAll: boolean) => { | ||||
| 		await bulkDownload( | ||||
| 			'immich-shared', | ||||
| 			isAll ? assets : Array.from(selectedAssets), | ||||
| 			() => { | ||||
| 				isMultiSelectionMode = false; | ||||
| 				clearMultiSelectAssetAssetHandler(); | ||||
| 			}, | ||||
| 			sharedLink?.key | ||||
| 		); | ||||
| 	const downloadAssets = async () => { | ||||
| 		await bulkDownload('immich-shared', assets, undefined, sharedLink?.key); | ||||
| 	}; | ||||
|  | ||||
| 	const handleUploadAssets = async () => { | ||||
| @@ -65,49 +55,16 @@ | ||||
| 			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> | ||||
|  | ||||
| <section class="bg-immich-bg dark:bg-immich-dark-bg"> | ||||
| 	{#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="Download" | ||||
| 					on:click={() => downloadAssets(false)} | ||||
| 					logo={CloudDownloadOutline} | ||||
| 				/> | ||||
| 				{#if isOwned} | ||||
| 					<CircleIconButton | ||||
| 						title="Remove from album" | ||||
| 						on:click={handleRemoveAssetsFromSharedLink} | ||||
| 						logo={DeleteOutline} | ||||
| 					/> | ||||
| 				{/if} | ||||
| 			</svelte:fragment> | ||||
| 		</ControlAppBar> | ||||
| 		<AssetSelectControlBar assets={selectedAssets} clearSelect={clearMultiSelectAssetAssetHandler}> | ||||
| 			<DownloadFiles filename="immich-shared" sharedLinkKey={sharedLink.key} /> | ||||
| 			{#if isOwned} | ||||
| 				<RemoveFromSharedLink bind:sharedLink allAssets={assets} /> | ||||
| 			{/if} | ||||
| 		</AssetSelectControlBar> | ||||
| 	{:else} | ||||
| 		<ControlAppBar | ||||
| 			on:close-button-click={() => goto('/photos')} | ||||
| @@ -139,7 +96,7 @@ | ||||
| 				{#if sharedLink?.allowDownload} | ||||
| 					<CircleIconButton | ||||
| 						title="Download" | ||||
| 						on:click={() => downloadAssets(true)} | ||||
| 						on:click={downloadAssets} | ||||
| 						logo={FolderDownloadOutline} | ||||
| 					/> | ||||
| 				{/if} | ||||
|   | ||||
| @@ -25,7 +25,7 @@ export const addAssetsToAlbum = async ( | ||||
| export async function bulkDownload( | ||||
| 	fileName: string, | ||||
| 	assets: AssetResponseDto[], | ||||
| 	onDone: () => void, | ||||
| 	onDone?: () => void, | ||||
| 	key?: string | ||||
| ) { | ||||
| 	const assetIds = assets.map((asset) => asset.id); | ||||
| @@ -63,7 +63,7 @@ export async function bulkDownload( | ||||
| 			if (isNotComplete && fileCount > 0) { | ||||
| 				// skip += fileCount; | ||||
| 			} else { | ||||
| 				onDone(); | ||||
| 				onDone?.(); | ||||
| 				done = true; | ||||
| 			} | ||||
|  | ||||
|   | ||||
| @@ -1,34 +1,27 @@ | ||||
| <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 type { PageData } from './$types'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { handleError } from '$lib/utils/handle-error'; | ||||
| 	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 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 { 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; | ||||
|  | ||||
| 	let selectedAssets: Set<AssetResponseDto> = new Set(); | ||||
| 	$: isMultiSelectionMode = selectedAssets.size > 0; | ||||
|  | ||||
| 	onMount(async () => { | ||||
| 		try { | ||||
| 			const { data: assets } = await api.assetApi.getAllAssets(undefined, true); | ||||
| @@ -38,144 +31,8 @@ | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	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') { | ||||
| 						$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; | ||||
| 	const onAssetDelete = (assetId: string) => { | ||||
| 		$archivedAsset = $archivedAsset.filter((a) => a.id !== assetId); | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| @@ -190,68 +47,20 @@ | ||||
|  | ||||
| 	<svelte:fragment slot="header"> | ||||
| 		{#if isMultiSelectionMode} | ||||
| 			<ControlAppBar | ||||
| 				on:close-button-click={clearMultiSelectAssetAssetHandler} | ||||
| 				backIcon={Close} | ||||
| 				tailwindClasses={'bg-white shadow-md'} | ||||
| 			<AssetSelectControlBar | ||||
| 				assets={selectedAssets} | ||||
| 				clearSelect={() => (selectedAssets = new Set())} | ||||
| 			> | ||||
| 				<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="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} | ||||
| 			/> | ||||
| 				<CreateSharedLink /> | ||||
| 				<RemoveFromArchive onAssetArchive={(asset) => onAssetDelete(asset.id)} /> | ||||
| 				<DownloadFiles /> | ||||
| 				<AssetSelectContextMenu icon={Plus} title="Add"> | ||||
| 					<OptionAddToFavorites /> | ||||
| 					<OptionAddToAlbum /> | ||||
| 					<OptionAddToAlbum shared /> | ||||
| 				</AssetSelectContextMenu> | ||||
| 				<DeleteAssets {onAssetDelete} /> | ||||
| 			</AssetSelectControlBar> | ||||
| 		{/if} | ||||
| 	</svelte:fragment> | ||||
|  | ||||
|   | ||||
| @@ -1,21 +1,17 @@ | ||||
| <script lang="ts"> | ||||
| 	import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.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 UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; | ||||
| 	import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.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 { handleError } from '$lib/utils/handle-error'; | ||||
| 	import { api, AssetResponseDto, SharedLinkType } from '@api'; | ||||
| 	import { api, AssetResponseDto } from '@api'; | ||||
| 	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 EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; | ||||
| 	import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; | ||||
| 	import type { PageData } from './$types'; | ||||
|  | ||||
| 	let favorites: AssetResponseDto[] = []; | ||||
| 	let isShowCreateSharedLinkModal = false; | ||||
| 	let selectedAssets: Set<AssetResponseDto> = new Set(); | ||||
|  | ||||
| 	export let data: PageData; | ||||
| @@ -31,69 +27,17 @@ | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	const clearMultiSelectAssetAssetHandler = () => { | ||||
| 		selectedAssets = new Set(); | ||||
| 	}; | ||||
|  | ||||
| 	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(); | ||||
| 	const onAssetDelete = (assetId: string) => { | ||||
| 		favorites = favorites.filter((a) => a.id !== assetId); | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <!-- Multiselection mode app bar --> | ||||
| {#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} | ||||
| 			</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} | ||||
| 	/> | ||||
| 	<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}> | ||||
| 		<CreateSharedLink /> | ||||
| 		<RemoveFavorite onAssetFavorite={(asset) => onAssetDelete(asset.id)} /> | ||||
| 	</AssetSelectControlBar> | ||||
| {/if} | ||||
|  | ||||
| <UserPageLayout user={data.user} hideNavbar={isMultiSelectionMode}> | ||||
|   | ||||
| @@ -1,52 +1,34 @@ | ||||
| <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 { bulkDownload } from '$lib/utils/asset-utils'; | ||||
| 	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 DownloadFiles from '$lib/components/photos-page/actions/download-files.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 { | ||||
| 		assetInteractionStore, | ||||
| 		isMultiSelectStoreState, | ||||
| 		selectedAssets | ||||
| 	} 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; | ||||
|  | ||||
| 	const handleDownloadFiles = async () => { | ||||
| 		await bulkDownload('immich', Array.from($selectedAssets), () => { | ||||
| 			assetInteractionStore.clearMultiselect(); | ||||
| 		}); | ||||
| 	}; | ||||
| 	onDestroy(() => { | ||||
| 		assetInteractionStore.clearMultiselect(); | ||||
| 	}); | ||||
| </script> | ||||
|  | ||||
| <main class="grid h-screen pt-[4.25rem] bg-immich-bg dark:bg-immich-dark-bg"> | ||||
| 	{#if $isMultiSelectStoreState} | ||||
| 		<ControlAppBar | ||||
| 			showBackButton | ||||
| 			backIcon={Close} | ||||
| 			on:close-button-click={() => assetInteractionStore.clearMultiselect()} | ||||
| 			tailwindClasses={'bg-white shadow-md'} | ||||
| 		<AssetSelectControlBar | ||||
| 			assets={$selectedAssets} | ||||
| 			clearSelect={assetInteractionStore.clearMultiselect} | ||||
| 		> | ||||
| 			<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="Download" | ||||
| 					logo={CloudDownloadOutline} | ||||
| 					on:click={handleDownloadFiles} | ||||
| 				/> | ||||
| 			</svelte:fragment> | ||||
| 		</ControlAppBar> | ||||
| 			<DownloadFiles /> | ||||
| 		</AssetSelectControlBar> | ||||
| 	{:else} | ||||
| 		<ControlAppBar | ||||
| 			showBackButton | ||||
|   | ||||
| @@ -1,235 +1,48 @@ | ||||
| <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 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 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 { | ||||
| 		assetInteractionStore, | ||||
| 		isMultiSelectStoreState, | ||||
| 		selectedAssets | ||||
| 	} from '$lib/stores/asset-interaction.store'; | ||||
| 	import { assetStore } from '$lib/stores/assets.store'; | ||||
| 	import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils'; | ||||
| 	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 { onDestroy } from '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'; | ||||
|  | ||||
| 	export let data: PageData; | ||||
|  | ||||
| 	let isShowCreateSharedLinkModal = false; | ||||
| 	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 | ||||
| 		}); | ||||
|  | ||||
| 	onDestroy(() => { | ||||
| 		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> | ||||
|  | ||||
| <UserPageLayout user={data.user} hideNavbar={$isMultiSelectStoreState} showUploadButton> | ||||
| 	<svelte:fragment slot="header"> | ||||
| 		{#if $isMultiSelectStoreState} | ||||
| 			<ControlAppBar | ||||
| 				on:close-button-click={() => assetInteractionStore.clearMultiselect()} | ||||
| 				backIcon={Close} | ||||
| 				tailwindClasses={'bg-white shadow-md'} | ||||
| 			<AssetSelectControlBar | ||||
| 				assets={$selectedAssets} | ||||
| 				clearSelect={assetInteractionStore.clearMultiselect} | ||||
| 			> | ||||
| 				<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="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} | ||||
| 			/> | ||||
| 				<CreateSharedLink /> | ||||
| 				<MoveToArchive onAssetArchive={(asset) => assetStore.removeAsset(asset.id)} /> | ||||
| 				<DownloadFiles /> | ||||
| 				<AssetSelectContextMenu icon={Plus} title="Add"> | ||||
| 					<OptionAddToFavorites /> | ||||
| 					<OptionAddToAlbum /> | ||||
| 					<OptionAddToAlbum shared /> | ||||
| 				</AssetSelectContextMenu> | ||||
| 				<DeleteAssets onAssetDelete={assetStore.removeAsset} /> | ||||
| 			</AssetSelectControlBar> | ||||
| 		{/if} | ||||
| 	</svelte:fragment> | ||||
|  | ||||
|   | ||||
| @@ -1,31 +1,25 @@ | ||||
| <script lang="ts"> | ||||
| 	import { afterNavigate, goto } from '$app/navigation'; | ||||
| 	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 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 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'; | ||||
| 	import type { PageData } from './$types'; | ||||
|  | ||||
| 	export let data: PageData; | ||||
|  | ||||
| 	// The GalleryViewer pushes it's own history state, which causes weird | ||||
| @@ -46,197 +40,34 @@ | ||||
| 	$: 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; | ||||
| 	const onAssetDelete = (assetId: string) => { | ||||
| 		searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => a.id !== assetId); | ||||
| 	}; | ||||
| </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> | ||||
| 		<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}> | ||||
| 			<CreateSharedLink /> | ||||
| 			{#if isAllArchived} | ||||
| 				<RemoveFromArchive /> | ||||
| 			{:else} | ||||
| 				<MoveToArchive /> | ||||
| 			{/if} | ||||
| 			<DownloadFiles /> | ||||
| 			<AssetSelectContextMenu icon={Plus} title="Add"> | ||||
| 				{#if isAllFavorite} | ||||
| 					<OptionRemoveFromFavorites /> | ||||
| 				{:else} | ||||
| 					<OptionAddToFavorites /> | ||||
| 				{/if} | ||||
| 				<OptionAddToAlbum /> | ||||
| 				<OptionAddToAlbum shared /> | ||||
| 			</AssetSelectContextMenu> | ||||
| 			<DeleteAssets {onAssetDelete} /> | ||||
| 		</AssetSelectControlBar> | ||||
| 	{:else} | ||||
| 		<ControlAppBar on:close-button-click={() => goto(previousRoute)} backIcon={ArrowLeft}> | ||||
| 			<div class="w-full max-w-2xl flex-1 pl-4"> | ||||
| @@ -271,35 +102,4 @@ | ||||
| 			{/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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user