Ver Fonte

feat(web): add current view asset to album (#923)

Jason Rasmussen há 2 anos atrás
pai
commit
5aa06ed3be

+ 39 - 0
web/src/lib/components/asset-viewer/album-list-item.svelte

@@ -0,0 +1,39 @@
+<script lang="ts">
+	import { AlbumResponseDto, ThumbnailFormat } from '@api';
+	import { createEventDispatcher } from 'svelte';
+
+	const dispatcher = createEventDispatcher();
+
+	export let album: AlbumResponseDto;
+	export let variant: 'simple' | 'full' = 'full';
+</script>
+
+<button
+	on:click={() => dispatcher('album')}
+	class="flex gap-4 px-6 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
+>
+	<div class="h-12 w-12">
+		<img
+			src={`/api/asset/thumbnail/${album.albumThumbnailAssetId}?format=${ThumbnailFormat.Webp}`}
+			alt={album.albumName}
+			class={`object-cover h-full w-full transition-all z-0 rounded-xl duration-300 hover:shadow-lg`}
+			data-testid="album-image"
+		/>
+	</div>
+	<div class="h-12 flex flex-col items-start justify-center">
+		<span>{album.albumName}</span>
+		<span class="flex gap-1 text-sm">
+			{#if variant === 'simple'}
+				<span
+					>{#if album.shared}Shared{/if}
+				</span>
+			{:else}
+				<span>{album.assetCount} items</span>
+				<span> · {new Date(album.createdAt).toLocaleDateString()}</span>
+				<span
+					>{#if album.shared} · Shared{/if}
+				</span>
+			{/if}
+		</span>
+	</div>
+</button>

+ 95 - 0
web/src/lib/components/asset-viewer/album-selection-modal.svelte

@@ -0,0 +1,95 @@
+<script lang="ts">
+	import { AlbumResponseDto, api } from '@api';
+	import { createEventDispatcher, onMount } from 'svelte';
+	import Plus from 'svelte-material-icons/Plus.svelte';
+	import BaseModal from '../shared-components/base-modal.svelte';
+	import AlbumListItem from './album-list-item.svelte';
+
+	let albums: AlbumResponseDto[] = [];
+	let recentAlbums: AlbumResponseDto[] = [];
+	let loading = true;
+
+	const dispatch = createEventDispatcher();
+
+	export let shared: boolean;
+
+	onMount(async () => {
+		const { data } = await api.albumApi.getAllAlbums();
+		albums = data;
+		recentAlbums = albums
+			.filter((album) => album.shared === shared)
+			.sort((a, b) => (new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1))
+			.slice(0, 3);
+		loading = false;
+	});
+
+	const handleSelect = (album: AlbumResponseDto) => {
+		dispatch('album', { album });
+	};
+
+	const handleNew = () => {
+		if (shared) {
+			dispatch('newAlbum');
+		} else {
+			dispatch('newSharedAlbum');
+		}
+	};
+</script>
+
+<BaseModal on:close={() => dispatch('close')}>
+	<svelte:fragment slot="title">
+		<span class="flex gap-2 place-items-center">
+			<p class="font-medium">
+				Add to {#if shared}shared {/if}
+			</p>
+		</span>
+	</svelte:fragment>
+
+	<div class=" max-h-[400px] overflow-y-auto immich-scrollbar">
+		<div class="flex flex-col mb-2">
+			{#if loading}
+				{#each { length: 3 } as _}
+					<div class="animate-pulse flex gap-4 px-6 py-2">
+						<div class="h-12 w-12 bg-slate-200 rounded-xl" />
+						<div class="flex flex-col items-start justify-center gap-2">
+							<span class="animate-pulse w-36 h-4 bg-slate-200" />
+							<div class="flex animate-pulse gap-1">
+								<span class="w-8 h-3 bg-slate-200" />
+								<span class="w-20 h-3 bg-slate-200" />
+							</div>
+						</div>
+					</div>
+				{/each}
+			{:else}
+				<button
+					on:click={handleNew}
+					class="flex gap-4 px-6 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors items-center"
+				>
+					<div class="h-12 w-12 flex justify-center items-center">
+						<Plus size="30" />
+					</div>
+					<p class="">
+						New {#if shared}Shared {/if}Album
+					</p>
+				</button>
+				{#if albums.length > 0}
+					<p class="text-sm font-medium px-5 py-1">RECENT</p>
+					{#each recentAlbums as album}
+						{#key album.id}
+							<AlbumListItem variant="simple" {album} on:album={() => handleSelect(album)} />
+						{/key}
+					{/each}
+
+					<p class="text-sm font-medium px-5 py-1">ALL ALBUMS</p>
+					{#each albums as album}
+						{#key album.id}
+							<AlbumListItem {album} on:album={() => handleSelect(album)} />
+						{/key}
+					{/each}
+				{:else}
+					<p class="text-sm px-5 py-1">It looks like you do not have any albums yet.</p>
+				{/if}
+			{/if}
+		</div>
+	</div>
+</BaseModal>

+ 31 - 0
web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte

@@ -4,9 +4,30 @@
 	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
 	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
 	import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
+	import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
 	import CircleIconButton from '../shared-components/circle-icon-button.svelte';
+	import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
+	import MenuOption from '../shared-components/context-menu/menu-option.svelte';
+
 	const dispatch = createEventDispatcher();
+
+	let contextMenuPosition = { x: 0, y: 0 };
+	let isShowAssetOptions = false;
+
+	const showOptionsMenu = (event: CustomEvent) => {
+		contextMenuPosition = {
+			x: event.detail.mouseEvent.x,
+			y: event.detail.mouseEvent.y
+		};
+
+		isShowAssetOptions = !isShowAssetOptions;
+	};
+
+	const onMenuClick = (eventName: string) => {
+		isShowAssetOptions = false;
+		dispatch(eventName);
+	};
 </script>
 
 <div
@@ -19,5 +40,15 @@
 		<CircleIconButton logo={CloudDownloadOutline} on:click={() => dispatch('download')} />
 		<CircleIconButton logo={DeleteOutline} on:click={() => dispatch('delete')} />
 		<CircleIconButton logo={InformationOutline} on:click={() => dispatch('showDetail')} />
+		<CircleIconButton logo={DotsVertical} on:click={(event) => showOptionsMenu(event)} />
 	</div>
 </div>
+
+{#if isShowAssetOptions}
+	<ContextMenu {...contextMenuPosition} on:clickoutside={() => (isShowAssetOptions = false)}>
+		<div class="flex flex-col rounded-lg ">
+			<MenuOption on:click={() => onMenuClick('addToAlbum')} text="Add to Album" />
+			<MenuOption on:click={() => onMenuClick('addToSharedAlbum')} text="Add to Shared Album" />
+		</div>
+	</ContextMenu>
+{/if}

+ 57 - 1
web/src/lib/components/asset-viewer/asset-viewer.svelte

@@ -6,9 +6,17 @@
 	import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
 	import PhotoViewer from './photo-viewer.svelte';
 	import DetailPanel from './detail-panel.svelte';
+	import { goto } from '$app/navigation';
 	import { downloadAssets } from '$lib/stores/download';
 	import VideoViewer from './video-viewer.svelte';
-	import { api, AssetResponseDto, AssetTypeEnum, AlbumResponseDto } from '@api';
+	import AlbumSelectionModal from './album-selection-modal.svelte';
+	import {
+		api,
+		AddAssetsResponseDto,
+		AssetResponseDto,
+		AssetTypeEnum,
+		AlbumResponseDto
+	} from '@api';
 	import {
 		notificationController,
 		NotificationType
@@ -29,6 +37,8 @@
 	let halfRightHover = false;
 	let isShowDetail = false;
 	let appearsInAlbums: AlbumResponseDto[] = [];
+	let isShowAlbumPicker = false;
+	let addToSharedAlbum = true;
 
 	const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo.key);
 
@@ -167,6 +177,39 @@
 			console.error('Error deleteSelectedAssetHandler', e);
 		}
 	};
+
+	const openAlbumPicker = (shared: boolean) => {
+		isShowAlbumPicker = true;
+		addToSharedAlbum = shared;
+	};
+
+	const showAddNotification = (dto: AddAssetsResponseDto) => {
+		notificationController.show({
+			message: `Added ${dto.successfullyAdded} to ${dto.album?.albumName}`,
+			type: NotificationType.Info
+		});
+
+		if (dto.successfullyAdded === 1 && dto.album) {
+			appearsInAlbums = [...appearsInAlbums, dto.album];
+		}
+	};
+
+	const handleAddToNewAlbum = () => {
+		isShowAlbumPicker = false;
+		api.albumApi.createAlbum({ albumName: 'Untitled', assetIds: [asset.id] }).then((response) => {
+			const album = response.data;
+			goto('/albums/' + album.id);
+		});
+	};
+
+	const handleAddToAlbum = async (event: CustomEvent<{ album: AlbumResponseDto }>) => {
+		isShowAlbumPicker = false;
+		const album = event.detail.album;
+
+		api.albumApi
+			.addAssetsToAlbum(album.id, { assetIds: [asset.id] })
+			.then((response) => showAddNotification(response.data));
+	};
 </script>
 
 <section
@@ -179,6 +222,8 @@
 			on:showDetail={showDetailInfoHandler}
 			on:download={downloadFile}
 			on:delete={deleteAsset}
+			on:addToAlbum={() => openAlbumPicker(false)}
+			on:addToSharedAlbum={() => openAlbumPicker(true)}
 		/>
 	</div>
 
@@ -246,6 +291,17 @@
 			<DetailPanel {asset} albums={appearsInAlbums} on:close={() => (isShowDetail = false)} />
 		</div>
 	{/if}
+
+	{#if isShowAlbumPicker}
+		<AlbumSelectionModal
+			shared={addToSharedAlbum}
+			on:newAlbum={handleAddToNewAlbum}
+			on:newSharedAlbum={handleAddToNewAlbum}
+			on:album={handleAddToAlbum}
+			on:close={() => (isShowAlbumPicker = false)}
+		/>
+		<div class="w-full h-full">Hello</div>
+	{/if}
 </section>
 
 <style>

+ 1 - 1
web/src/lib/components/shared-components/base-modal.svelte

@@ -38,7 +38,7 @@
 		on:out-click={() => dispatch('close')}
 		class="bg-immich-bg dark:bg-immich-dark-gray dark:text-immich-dark-fg w-[450px] min-h-[200px] max-h-[500px] rounded-lg shadow-md"
 	>
-		<div class="flex justify-between place-items-center p-5">
+		<div class="flex justify-between place-items-center px-5 py-3">
 			<div>
 				<slot name="title">
 					<p>Modal Title</p>

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

@@ -32,7 +32,7 @@
 <div
 	transition:slide={{ duration: 200, easing: quintOut }}
 	bind:this={menuEl}
-	class="absolute w-[175px] z-[99999] rounded-lg shadow-md"
+	class="absolute w-[200px] z-[99999] rounded-lg overflow-hidden"
 	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-gray-300 dark:text-immich-dark-bg transition-all p-4 w-full text-left rounded-lg text-sm"
+	class="bg-white hover:bg-gray-300 dark:text-immich-dark-bg transition-all p-4 w-full text-left text-sm"
 >
 	{#if text}
 		{text}