mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Delete album on web (#373)
* Show context menu * Show context menu at the correct location * Implement delete album button * Delete album within album viewer
This commit is contained in:
		@@ -1,7 +1,9 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import { AlbumResponseDto, api, ThumbnailFormat } from '@api';
 | 
						import { AlbumResponseDto, api, ThumbnailFormat } from '@api';
 | 
				
			||||||
	import { createEventDispatcher, onMount } from 'svelte';
 | 
						import { createEventDispatcher, onMount } from 'svelte';
 | 
				
			||||||
 | 
						import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 | 
				
			||||||
	import { fade } from 'svelte/transition';
 | 
						import { fade } from 'svelte/transition';
 | 
				
			||||||
 | 
						import CircleIconButton from '../shared-components/circle-icon-button.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let album: AlbumResponseDto;
 | 
						export let album: AlbumResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -21,13 +23,33 @@
 | 
				
			|||||||
			return imageData;
 | 
								return imageData;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const showAlbumContextMenu = (e: MouseEvent) => {
 | 
				
			||||||
 | 
							dispatch('showalbumcontextmenu', {
 | 
				
			||||||
 | 
								x: e.clientX,
 | 
				
			||||||
 | 
								y: e.clientY
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div
 | 
					<div
 | 
				
			||||||
	class="h-[339px] w-[275px] hover:cursor-pointer mt-4"
 | 
						class="h-[339px] w-[275px] hover:cursor-pointer mt-4 relative"
 | 
				
			||||||
	on:click={() => dispatch('click', album)}
 | 
						on:click={() => dispatch('click', album)}
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<div class={`h-[275px] w-[275px]`}>
 | 
						<div
 | 
				
			||||||
 | 
							id={`icon-${album.id}`}
 | 
				
			||||||
 | 
							class="absolute top-2 right-2"
 | 
				
			||||||
 | 
							on:click|stopPropagation|preventDefault={showAlbumContextMenu}
 | 
				
			||||||
 | 
						>
 | 
				
			||||||
 | 
							<CircleIconButton
 | 
				
			||||||
 | 
								logo={DotsVertical}
 | 
				
			||||||
 | 
								size={'20'}
 | 
				
			||||||
 | 
								hoverColor={'rgba(95,99,104, 0.5)'}
 | 
				
			||||||
 | 
								logoColor={'#fdf8ec'}
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div class={`h-[275px] w-[275px] z-[-1]`}>
 | 
				
			||||||
		{#await loadImageData(album.albumThumbnailAssetId)}
 | 
							{#await loadImageData(album.albumThumbnailAssetId)}
 | 
				
			||||||
			<div
 | 
								<div
 | 
				
			||||||
				class={`bg-immich-primary/10 w-full h-full  flex place-items-center place-content-center rounded-xl`}
 | 
									class={`bg-immich-primary/10 w-full h-full  flex place-items-center place-content-center rounded-xl`}
 | 
				
			||||||
@@ -39,7 +61,7 @@
 | 
				
			|||||||
				in:fade={{ duration: 250 }}
 | 
									in:fade={{ duration: 250 }}
 | 
				
			||||||
				src={imageData}
 | 
									src={imageData}
 | 
				
			||||||
				alt={album.id}
 | 
									alt={album.id}
 | 
				
			||||||
				class={`object-cover w-full h-full transition-all z-0 rounded-xl duration-300 hover:translate-x-2 hover:-translate-y-2 hover:shadow-[-8px_8px_0px_0_#FFB800]`}
 | 
									class={`object-cover w-full h-full transition-all z-0 rounded-xl duration-300 hover:shadow-lg`}
 | 
				
			||||||
			/>
 | 
								/>
 | 
				
			||||||
		{/await}
 | 
							{/await}
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
@@ -59,6 +81,3 @@
 | 
				
			|||||||
		</span>
 | 
							</span>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -222,6 +222,21 @@
 | 
				
			|||||||
			console.log('Error [sharedUserDeletedHandler] ', e);
 | 
								console.log('Error [sharedUserDeletedHandler] ', e);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const removeAlbum = async () => {
 | 
				
			||||||
 | 
							if (
 | 
				
			||||||
 | 
								window.confirm(
 | 
				
			||||||
 | 
									`Are you sure you want to delete album ${album.albumName}? If the album is shared, other users will not be able to access it.`
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									await api.albumApi.deleteAlbum(album.id);
 | 
				
			||||||
 | 
									goto(backUrl);
 | 
				
			||||||
 | 
								} catch (e) {
 | 
				
			||||||
 | 
									console.log('Error [userDeleteMenu] ', e);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<section class="bg-immich-bg">
 | 
					<section class="bg-immich-bg">
 | 
				
			||||||
@@ -265,6 +280,7 @@
 | 
				
			|||||||
							on:click={() => (isShowShareUserSelection = true)}
 | 
												on:click={() => (isShowShareUserSelection = true)}
 | 
				
			||||||
							logo={ShareVariantOutline}
 | 
												logo={ShareVariantOutline}
 | 
				
			||||||
						/>
 | 
											/>
 | 
				
			||||||
 | 
											<CircleIconButton title="Remove album" on:click={removeAlbum} logo={DeleteOutline} />
 | 
				
			||||||
					{/if}
 | 
										{/if}
 | 
				
			||||||
				{/if}
 | 
									{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,7 @@
 | 
				
			|||||||
	{title}
 | 
						{title}
 | 
				
			||||||
	bind:this={iconButton}
 | 
						bind:this={iconButton}
 | 
				
			||||||
	class={`immich-circle-icon-button rounded-full p-3 flex place-items-center place-content-center transition-all`}
 | 
						class={`immich-circle-icon-button rounded-full p-3 flex place-items-center place-content-center transition-all`}
 | 
				
			||||||
	on:click={() => dispatch('click')}
 | 
						on:click={(mouseEvent) => dispatch('click', { mouseEvent })}
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<svelte:component this={logo} {size} color={logoColor} />
 | 
						<svelte:component this={logo} {size} color={logoColor} />
 | 
				
			||||||
</button>
 | 
					</button>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,10 +4,20 @@
 | 
				
			|||||||
	import { quintOut } from 'svelte/easing';
 | 
						import { quintOut } from 'svelte/easing';
 | 
				
			||||||
	import { slide } from 'svelte/transition';
 | 
						import { slide } from 'svelte/transition';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * x coordiante of the context menu.
 | 
				
			||||||
 | 
						 * @type {number}
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
	export let x: number = 0;
 | 
						export let x: number = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * x coordiante of the context menu.
 | 
				
			||||||
 | 
						 * @type {number}
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
	export let y: number = 0;
 | 
						export let y: number = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const dispatch = createEventDispatcher();
 | 
						const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let menuEl: HTMLElement;
 | 
						let menuEl: HTMLElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$: (() => {
 | 
						$: (() => {
 | 
				
			||||||
@@ -24,7 +34,7 @@
 | 
				
			|||||||
<div
 | 
					<div
 | 
				
			||||||
	transition:slide={{ duration: 200, easing: quintOut }}
 | 
						transition:slide={{ duration: 200, easing: quintOut }}
 | 
				
			||||||
	bind:this={menuEl}
 | 
						bind:this={menuEl}
 | 
				
			||||||
	class="absolute bg-white w-[150px] z-[99999] rounded-lg shadow-md"
 | 
						class="absolute bg-white w-[175px] z-[99999] rounded-lg shadow-md"
 | 
				
			||||||
	style={`top: ${y}px; left: ${x}px;`}
 | 
						style={`top: ${y}px; left: ${x}px;`}
 | 
				
			||||||
	use:clickOutside
 | 
						use:clickOutside
 | 
				
			||||||
	on:out-click={() => dispatch('clickoutside')}
 | 
						on:out-click={() => dispatch('clickoutside')}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@
 | 
				
			|||||||
<button
 | 
					<button
 | 
				
			||||||
	class:disabled={isDisabled}
 | 
						class:disabled={isDisabled}
 | 
				
			||||||
	on:click={handleClick}
 | 
						on:click={handleClick}
 | 
				
			||||||
	class="bg-white hover:bg-immich-bg transition-all p-4 w-full text-left rounded-lg"
 | 
						class="bg-white hover:bg-immich-bg transition-all p-4 w-full text-left rounded-lg text-sm"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	{#if text}
 | 
						{#if text}
 | 
				
			||||||
		{text}
 | 
							{text}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,10 +39,17 @@
 | 
				
			|||||||
	import AlbumCard from '$lib/components/album-page/album-card.svelte';
 | 
						import AlbumCard from '$lib/components/album-page/album-card.svelte';
 | 
				
			||||||
	import { goto } from '$app/navigation';
 | 
						import { goto } from '$app/navigation';
 | 
				
			||||||
	import { onMount } from 'svelte';
 | 
						import { onMount } from '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 DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let user: ImmichUser;
 | 
						export let user: ImmichUser;
 | 
				
			||||||
	export let albums: AlbumResponseDto[];
 | 
						export let albums: AlbumResponseDto[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let isShowContextMenu = false;
 | 
				
			||||||
 | 
						let contextMenuPosition = { x: 0, y: 0 };
 | 
				
			||||||
 | 
						let targetAlbum: AlbumResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	onMount(async () => {
 | 
						onMount(async () => {
 | 
				
			||||||
		const { data } = await api.albumApi.getAllAlbums();
 | 
							const { data } = await api.albumApi.getAllAlbums();
 | 
				
			||||||
		albums = data;
 | 
							albums = data;
 | 
				
			||||||
@@ -50,7 +57,7 @@
 | 
				
			|||||||
		// Delete album that has no photos and is named 'Untitled'
 | 
							// Delete album that has no photos and is named 'Untitled'
 | 
				
			||||||
		for (const album of albums) {
 | 
							for (const album of albums) {
 | 
				
			||||||
			if (album.albumName === 'Untitled' && album.assets.length === 0) {
 | 
								if (album.albumName === 'Untitled' && album.assets.length === 0) {
 | 
				
			||||||
				const isDeleted = await deleteAlbum(album);
 | 
									const isDeleted = await autoDeleteAlbum(album);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (isDeleted) {
 | 
									if (isDeleted) {
 | 
				
			||||||
					albums = albums.filter((a) => a.id !== album.id);
 | 
										albums = albums.filter((a) => a.id !== album.id);
 | 
				
			||||||
@@ -71,15 +78,43 @@
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const deleteAlbum = async (album: AlbumResponseDto) => {
 | 
						const autoDeleteAlbum = async (album: AlbumResponseDto) => {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			await api.albumApi.deleteAlbum(album.id);
 | 
								await api.albumApi.deleteAlbum(album.id);
 | 
				
			||||||
			return true;
 | 
								return true;
 | 
				
			||||||
		} catch (e) {
 | 
							} catch (e) {
 | 
				
			||||||
			console.log('Error [deleteAlbum] ', e);
 | 
								console.log('Error [autoDeleteAlbum] ', e);
 | 
				
			||||||
			return false;
 | 
								return false;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const userDeleteMenu = async () => {
 | 
				
			||||||
 | 
							if (
 | 
				
			||||||
 | 
								window.confirm(
 | 
				
			||||||
 | 
									`Are you sure you want to delete album ${targetAlbum.albumName}? If the album is shared, other users will not be able to access it.`
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									await api.albumApi.deleteAlbum(targetAlbum.id);
 | 
				
			||||||
 | 
									albums = albums.filter((a) => a.id !== targetAlbum.id);
 | 
				
			||||||
 | 
								} catch (e) {
 | 
				
			||||||
 | 
									console.log('Error [userDeleteMenu] ', e);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							isShowContextMenu = false;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const showAlbumContextMenu = (event: CustomEvent, album: AlbumResponseDto) => {
 | 
				
			||||||
 | 
							targetAlbum = album;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							contextMenuPosition = {
 | 
				
			||||||
 | 
								x: event.detail.x,
 | 
				
			||||||
 | 
								y: event.detail.y
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							isShowContextMenu = !isShowContextMenu;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<svelte:head>
 | 
					<svelte:head>
 | 
				
			||||||
@@ -119,11 +154,25 @@
 | 
				
			|||||||
			<!-- Album Card -->
 | 
								<!-- Album Card -->
 | 
				
			||||||
			<div class="flex flex-wrap gap-8">
 | 
								<div class="flex flex-wrap gap-8">
 | 
				
			||||||
				{#each albums as album}
 | 
									{#each albums as album}
 | 
				
			||||||
 | 
										{#key album.id}
 | 
				
			||||||
						<a sveltekit:prefetch href={`albums/${album.id}`}>
 | 
											<a sveltekit:prefetch href={`albums/${album.id}`}>
 | 
				
			||||||
						<AlbumCard {album} />
 | 
												<AlbumCard {album} on:showalbumcontextmenu={(e) => showAlbumContextMenu(e, album)} />
 | 
				
			||||||
						</a>
 | 
											</a>
 | 
				
			||||||
 | 
										{/key}
 | 
				
			||||||
				{/each}
 | 
									{/each}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</section>
 | 
							</section>
 | 
				
			||||||
	</section>
 | 
						</section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<!-- Context Menu -->
 | 
				
			||||||
 | 
						{#if isShowContextMenu}
 | 
				
			||||||
 | 
							<ContextMenu {...contextMenuPosition} on:clickoutside={() => (isShowContextMenu = false)}>
 | 
				
			||||||
 | 
								<MenuOption on:click={userDeleteMenu}>
 | 
				
			||||||
 | 
									<span class="flex place-items-center place-content-center gap-2">
 | 
				
			||||||
 | 
										<DeleteOutline size="18" />
 | 
				
			||||||
 | 
										<p>Delete album</p>
 | 
				
			||||||
 | 
									</span>
 | 
				
			||||||
 | 
								</MenuOption>
 | 
				
			||||||
 | 
							</ContextMenu>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
</section>
 | 
					</section>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user