Jelajahi Sumber

Delete album on web (#373)

* Show context menu

* Show context menu at the correct location

* Implement delete album button

* Delete album within album viewer
Alex 3 tahun lalu
induk
melakukan
969f770df0

+ 25 - 6
web/src/lib/components/album-page/album-card.svelte

@@ -1,7 +1,9 @@
 <script lang="ts">
 	import { AlbumResponseDto, api, ThumbnailFormat } from '@api';
 	import { createEventDispatcher, onMount } from 'svelte';
+	import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 	import { fade } from 'svelte/transition';
+	import CircleIconButton from '../shared-components/circle-icon-button.svelte';
 
 	export let album: AlbumResponseDto;
 
@@ -21,13 +23,33 @@
 			return imageData;
 		}
 	};
+
+	const showAlbumContextMenu = (e: MouseEvent) => {
+		dispatch('showalbumcontextmenu', {
+			x: e.clientX,
+			y: e.clientY
+		});
+	};
 </script>
 
 <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)}
 >
-	<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)}
 			<div
 				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 }}
 				src={imageData}
 				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}
 	</div>
@@ -59,6 +81,3 @@
 		</span>
 	</div>
 </div>
-
-<style>
-</style>

+ 16 - 0
web/src/lib/components/album-page/album-viewer.svelte

@@ -222,6 +222,21 @@
 			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>
 
 <section class="bg-immich-bg">
@@ -265,6 +280,7 @@
 							on:click={() => (isShowShareUserSelection = true)}
 							logo={ShareVariantOutline}
 						/>
+						<CircleIconButton title="Remove album" on:click={removeAlbum} logo={DeleteOutline} />
 					{/if}
 				{/if}
 

+ 1 - 1
web/src/lib/components/shared-components/circle-icon-button.svelte

@@ -25,7 +25,7 @@
 	{title}
 	bind:this={iconButton}
 	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} />
 </button>

+ 11 - 1
web/src/lib/components/shared-components/context-menu/context-menu.svelte

@@ -4,10 +4,20 @@
 	import { quintOut } from 'svelte/easing';
 	import { slide } from 'svelte/transition';
 
+	/**
+	 * x coordiante of the context menu.
+	 * @type {number}
+	 */
 	export let x: number = 0;
+
+	/**
+	 * x coordiante of the context menu.
+	 * @type {number}
+	 */
 	export let y: number = 0;
 
 	const dispatch = createEventDispatcher();
+
 	let menuEl: HTMLElement;
 
 	$: (() => {
@@ -24,7 +34,7 @@
 <div
 	transition:slide={{ duration: 200, easing: quintOut }}
 	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;`}
 	use:clickOutside
 	on:out-click={() => dispatch('clickoutside')}

+ 1 - 1
web/src/lib/components/shared-components/context-menu/menu-option.svelte

@@ -16,7 +16,7 @@
 <button
 	class:disabled={isDisabled}
 	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}
 		{text}

+ 55 - 6
web/src/routes/albums/index.svelte

@@ -39,10 +39,17 @@
 	import AlbumCard from '$lib/components/album-page/album-card.svelte';
 	import { goto } from '$app/navigation';
 	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 albums: AlbumResponseDto[];
 
+	let isShowContextMenu = false;
+	let contextMenuPosition = { x: 0, y: 0 };
+	let targetAlbum: AlbumResponseDto;
+
 	onMount(async () => {
 		const { data } = await api.albumApi.getAllAlbums();
 		albums = data;
@@ -50,7 +57,7 @@
 		// Delete album that has no photos and is named 'Untitled'
 		for (const album of albums) {
 			if (album.albumName === 'Untitled' && album.assets.length === 0) {
-				const isDeleted = await deleteAlbum(album);
+				const isDeleted = await autoDeleteAlbum(album);
 
 				if (isDeleted) {
 					albums = albums.filter((a) => a.id !== album.id);
@@ -71,15 +78,43 @@
 		}
 	};
 
-	const deleteAlbum = async (album: AlbumResponseDto) => {
+	const autoDeleteAlbum = async (album: AlbumResponseDto) => {
 		try {
 			await api.albumApi.deleteAlbum(album.id);
 			return true;
 		} catch (e) {
-			console.log('Error [deleteAlbum] ', e);
+			console.log('Error [autoDeleteAlbum] ', e);
 			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>
 
 <svelte:head>
@@ -119,11 +154,25 @@
 			<!-- Album Card -->
 			<div class="flex flex-wrap gap-8">
 				{#each albums as album}
-					<a sveltekit:prefetch href={`albums/${album.id}`}>
-						<AlbumCard {album} />
-					</a>
+					{#key album.id}
+						<a sveltekit:prefetch href={`albums/${album.id}`}>
+							<AlbumCard {album} on:showalbumcontextmenu={(e) => showAlbumContextMenu(e, album)} />
+						</a>
+					{/key}
 				{/each}
 			</div>
 		</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>