mirror of
https://github.com/immich-app/immich.git
synced 2024-11-15 09:59:00 -07:00
feat(web): add current view asset to album (#923)
This commit is contained in:
parent
d696ce4e41
commit
5aa06ed3be
39
web/src/lib/components/asset-viewer/album-list-item.svelte
Normal file
39
web/src/lib/components/asset-viewer/album-list-item.svelte
Normal file
@ -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>
|
@ -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>
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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')}
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user