forked from Cutlery/immich
		
	refactor(web): albums list (1) (#7660)
* refactor: albums list * fix: rename filename * chore: fix merge * pr feedback * chore: fix merge * pr feedback --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									2080aeee4d
								
							
						
					
					
						commit
						c04dfdf38b
					
				
							
								
								
									
										68
									
								
								web/src/lib/components/album-page/albums-controls.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								web/src/lib/components/album-page/albums-controls.svelte
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; | ||||||
|  |   import Dropdown from '$lib/components/elements/dropdown.svelte'; | ||||||
|  |   import Icon from '$lib/components/elements/icon.svelte'; | ||||||
|  |   import { AlbumViewMode, albumViewSettings } from '$lib/stores/preferences.store'; | ||||||
|  |   import { | ||||||
|  |     mdiArrowDownThin, | ||||||
|  |     mdiArrowUpThin, | ||||||
|  |     mdiFormatListBulletedSquare, | ||||||
|  |     mdiPlusBoxOutline, | ||||||
|  |     mdiViewGridOutline, | ||||||
|  |   } from '@mdi/js'; | ||||||
|  |   import { sortByOptions, type Sort, handleCreateAlbum } from '$lib/components/album-page/albums-list.svelte'; | ||||||
|  |   import SearchBar from '$lib/components/elements/search-bar.svelte'; | ||||||
|  | 
 | ||||||
|  |   export let searchAlbum: string; | ||||||
|  | 
 | ||||||
|  |   const searchSort = (searched: string): Sort => { | ||||||
|  |     return sortByOptions.find((option) => option.title === searched) || sortByOptions[0]; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleChangeListMode = () => { | ||||||
|  |     $albumViewSettings.view = | ||||||
|  |       $albumViewSettings.view === AlbumViewMode.Cover ? AlbumViewMode.List : AlbumViewMode.Cover; | ||||||
|  |   }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class="hidden lg:block lg:w-40 xl:w-60 2xl:w-80 h-10"> | ||||||
|  |   <SearchBar placeholder="Search albums" bind:name={searchAlbum} isSearching={false} /> | ||||||
|  | </div> | ||||||
|  | <LinkButton on:click={handleCreateAlbum}> | ||||||
|  |   <div class="flex place-items-center gap-2 text-sm"> | ||||||
|  |     <Icon path={mdiPlusBoxOutline} size="18" /> | ||||||
|  |     Create album | ||||||
|  |   </div> | ||||||
|  | </LinkButton> | ||||||
|  | 
 | ||||||
|  | <Dropdown | ||||||
|  |   options={Object.values(sortByOptions)} | ||||||
|  |   selectedOption={searchSort($albumViewSettings.sortBy)} | ||||||
|  |   render={(option) => { | ||||||
|  |     return { | ||||||
|  |       title: option.title, | ||||||
|  |       icon: option.sortDesc ? mdiArrowDownThin : mdiArrowUpThin, | ||||||
|  |     }; | ||||||
|  |   }} | ||||||
|  |   on:select={(event) => { | ||||||
|  |     for (const key of sortByOptions) { | ||||||
|  |       if (key.title === event.detail.title) { | ||||||
|  |         key.sortDesc = !key.sortDesc; | ||||||
|  |         $albumViewSettings.sortBy = key.title; | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }} | ||||||
|  | /> | ||||||
|  | 
 | ||||||
|  | <LinkButton on:click={() => handleChangeListMode()}> | ||||||
|  |   <div class="flex place-items-center gap-2 text-sm"> | ||||||
|  |     {#if $albumViewSettings.view === AlbumViewMode.List} | ||||||
|  |       <Icon path={mdiViewGridOutline} size="18" /> | ||||||
|  |       <p class="hidden sm:block">Cover</p> | ||||||
|  |     {:else} | ||||||
|  |       <Icon path={mdiFormatListBulletedSquare} size="18" /> | ||||||
|  |       <p class="hidden sm:block">List</p> | ||||||
|  |     {/if} | ||||||
|  |   </div> | ||||||
|  | </LinkButton> | ||||||
							
								
								
									
										282
									
								
								web/src/lib/components/album-page/albums-list.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								web/src/lib/components/album-page/albums-list.svelte
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,282 @@ | |||||||
|  | <script lang="ts" context="module"> | ||||||
|  |   import { AlbumViewMode, albumViewSettings } from '$lib/stores/preferences.store'; | ||||||
|  |   import { goto } from '$app/navigation'; | ||||||
|  |   import type { OnShowContextMenuDetail } from '$lib/components/album-page/album-card'; | ||||||
|  |   import { AppRoute } from '$lib/constants'; | ||||||
|  |   import { createAlbum, deleteAlbum, type AlbumResponseDto } from '@immich/sdk'; | ||||||
|  |   import { get } from 'svelte/store'; | ||||||
|  | 
 | ||||||
|  |   export const handleCreateAlbum = async () => { | ||||||
|  |     try { | ||||||
|  |       const newAlbum = await createAlbum({ createAlbumDto: { albumName: '' } }); | ||||||
|  | 
 | ||||||
|  |       await goto(`${AppRoute.ALBUMS}/${newAlbum.id}`); | ||||||
|  |     } catch (error) { | ||||||
|  |       handleError(error, 'Unable to create album'); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   export interface Sort { | ||||||
|  |     title: string; | ||||||
|  |     sortDesc: boolean; | ||||||
|  |     widthClass: string; | ||||||
|  |     sortFn: (reverse: boolean, albums: AlbumResponseDto[]) => AlbumResponseDto[]; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   export let sortByOptions: Sort[] = [ | ||||||
|  |     { | ||||||
|  |       title: 'Album title', | ||||||
|  |       sortDesc: get(albumViewSettings).sortDesc, // Load Sort Direction | ||||||
|  |       widthClass: 'text-left w-8/12 sm:w-4/12 md:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]', | ||||||
|  |       sortFn: (reverse, albums) => { | ||||||
|  |         return orderBy(albums, 'albumName', [reverse ? 'desc' : 'asc']); | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: 'Number of assets', | ||||||
|  |       sortDesc: get(albumViewSettings).sortDesc, | ||||||
|  |       widthClass: 'text-center w-4/12 m:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]', | ||||||
|  |       sortFn: (reverse, albums) => { | ||||||
|  |         return orderBy(albums, 'assetCount', [reverse ? 'desc' : 'asc']); | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: 'Last modified', | ||||||
|  |       sortDesc: get(albumViewSettings).sortDesc, | ||||||
|  |       widthClass: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]', | ||||||
|  |       sortFn: (reverse, albums) => { | ||||||
|  |         return orderBy(albums, [(album) => new Date(album.updatedAt)], [reverse ? 'desc' : 'asc']); | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: 'Created date', | ||||||
|  |       sortDesc: get(albumViewSettings).sortDesc, | ||||||
|  |       widthClass: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]', | ||||||
|  |       sortFn: (reverse, albums) => { | ||||||
|  |         return orderBy(albums, [(album) => new Date(album.createdAt)], [reverse ? 'desc' : 'asc']); | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: 'Most recent photo', | ||||||
|  |       sortDesc: get(albumViewSettings).sortDesc, | ||||||
|  |       widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]', | ||||||
|  |       sortFn: (reverse, albums) => { | ||||||
|  |         return orderBy( | ||||||
|  |           albums, | ||||||
|  |           [(album) => (album.endDate ? new Date(album.endDate) : '')], | ||||||
|  |           [reverse ? 'desc' : 'asc'], | ||||||
|  |         ).sort((a, b) => { | ||||||
|  |           if (a.endDate === undefined) { | ||||||
|  |             return 1; | ||||||
|  |           } | ||||||
|  |           if (b.endDate === undefined) { | ||||||
|  |             return -1; | ||||||
|  |           } | ||||||
|  |           return 0; | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: 'Oldest photo', | ||||||
|  |       sortDesc: get(albumViewSettings).sortDesc, | ||||||
|  |       widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]', | ||||||
|  |       sortFn: (reverse, albums) => { | ||||||
|  |         return orderBy( | ||||||
|  |           albums, | ||||||
|  |           [(album) => (album.startDate ? new Date(album.startDate) : null)], | ||||||
|  |           [reverse ? 'desc' : 'asc'], | ||||||
|  |         ).sort((a, b) => { | ||||||
|  |           if (a.startDate === undefined) { | ||||||
|  |             return 1; | ||||||
|  |           } | ||||||
|  |           if (b.startDate === undefined) { | ||||||
|  |             return -1; | ||||||
|  |           } | ||||||
|  |           return 0; | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   ]; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  |   import AlbumCard from '$lib/components/album-page/album-card.svelte'; | ||||||
|  |   import Icon from '$lib/components/elements/icon.svelte'; | ||||||
|  |   import EditAlbumForm from '$lib/components/forms/edit-album-form.svelte'; | ||||||
|  |   import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; | ||||||
|  |   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; | ||||||
|  |   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; | ||||||
|  |   import { | ||||||
|  |     NotificationType, | ||||||
|  |     notificationController, | ||||||
|  |   } from '$lib/components/shared-components/notification/notification'; | ||||||
|  |   import { mdiDeleteOutline } from '@mdi/js'; | ||||||
|  |   import { orderBy } from 'lodash-es'; | ||||||
|  |   import { onMount } from 'svelte'; | ||||||
|  |   import { flip } from 'svelte/animate'; | ||||||
|  |   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; | ||||||
|  |   import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'; | ||||||
|  |   import AlbumsTable from '$lib/components/album-page/albums-table.svelte'; | ||||||
|  |   import { handleError } from '$lib/utils/handle-error'; | ||||||
|  | 
 | ||||||
|  |   export let albums: AlbumResponseDto[]; | ||||||
|  |   export let searchAlbum: string; | ||||||
|  | 
 | ||||||
|  |   let shouldShowEditAlbumForm = false; | ||||||
|  |   let selectedAlbum: AlbumResponseDto; | ||||||
|  |   let albumToDelete: AlbumResponseDto | null; | ||||||
|  |   let contextMenuPosition: OnShowContextMenuDetail = { x: 0, y: 0 }; | ||||||
|  |   let contextMenuTargetAlbum: AlbumResponseDto | undefined = undefined; | ||||||
|  | 
 | ||||||
|  |   $: { | ||||||
|  |     for (const key of sortByOptions) { | ||||||
|  |       if (key.title === $albumViewSettings.sortBy) { | ||||||
|  |         albums = key.sortFn(key.sortDesc, albums); | ||||||
|  |         $albumViewSettings.sortDesc = key.sortDesc; // "Save" sortDesc | ||||||
|  |         $albumViewSettings.sortBy = key.title; | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   $: isShowContextMenu = !!contextMenuTargetAlbum; | ||||||
|  |   $: albumsFiltered = albums.filter((album) => album.albumName.toLowerCase().includes(searchAlbum.toLowerCase())); | ||||||
|  | 
 | ||||||
|  |   onMount(async () => { | ||||||
|  |     await removeAlbumsIfEmpty(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   function showAlbumContextMenu(contextMenuDetail: OnShowContextMenuDetail, album: AlbumResponseDto): void { | ||||||
|  |     contextMenuTargetAlbum = album; | ||||||
|  |     contextMenuPosition = { | ||||||
|  |       x: contextMenuDetail.x, | ||||||
|  |       y: contextMenuDetail.y, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function closeAlbumContextMenu() { | ||||||
|  |     contextMenuTargetAlbum = undefined; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function handleDeleteAlbum(albumToDelete: AlbumResponseDto): Promise<void> { | ||||||
|  |     await deleteAlbum({ id: albumToDelete.id }); | ||||||
|  |     albums = albums.filter(({ id }) => id !== albumToDelete.id); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const chooseAlbumToDelete = (album: AlbumResponseDto) => { | ||||||
|  |     contextMenuTargetAlbum = album; | ||||||
|  |     setAlbumToDelete(); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const setAlbumToDelete = () => { | ||||||
|  |     albumToDelete = contextMenuTargetAlbum ?? null; | ||||||
|  |     closeAlbumContextMenu(); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleEdit = (album: AlbumResponseDto) => { | ||||||
|  |     selectedAlbum = { ...album }; | ||||||
|  |     shouldShowEditAlbumForm = true; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const deleteSelectedAlbum = async () => { | ||||||
|  |     if (!albumToDelete) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     try { | ||||||
|  |       await handleDeleteAlbum(albumToDelete); | ||||||
|  |     } catch { | ||||||
|  |       notificationController.show({ | ||||||
|  |         message: 'Error deleting album', | ||||||
|  |         type: NotificationType.Error, | ||||||
|  |       }); | ||||||
|  |     } finally { | ||||||
|  |       albumToDelete = null; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const removeAlbumsIfEmpty = async () => { | ||||||
|  |     for (const album of albums) { | ||||||
|  |       if (album.assetCount == 0 && album.albumName == '') { | ||||||
|  |         try { | ||||||
|  |           await handleDeleteAlbum(album); | ||||||
|  |         } catch (error) { | ||||||
|  |           console.log(error); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const successModifyAlbum = () => { | ||||||
|  |     shouldShowEditAlbumForm = false; | ||||||
|  |     notificationController.show({ | ||||||
|  |       message: 'Album infos updated', | ||||||
|  |       type: NotificationType.Info, | ||||||
|  |     }); | ||||||
|  |     albums[albums.findIndex((x) => x.id === selectedAlbum.id)] = selectedAlbum; | ||||||
|  |   }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if shouldShowEditAlbumForm} | ||||||
|  |   <FullScreenModal onClose={() => (shouldShowEditAlbumForm = false)}> | ||||||
|  |     <EditAlbumForm | ||||||
|  |       album={selectedAlbum} | ||||||
|  |       on:editSuccess={() => successModifyAlbum()} | ||||||
|  |       on:cancel={() => (shouldShowEditAlbumForm = false)} | ||||||
|  |     /> | ||||||
|  |   </FullScreenModal> | ||||||
|  | {/if} | ||||||
|  | 
 | ||||||
|  | {#if albums.length > 0} | ||||||
|  |   <!-- Album Card --> | ||||||
|  |   {#if $albumViewSettings.view === AlbumViewMode.Cover} | ||||||
|  |     <div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]"> | ||||||
|  |       {#each albumsFiltered as album, index (album.id)} | ||||||
|  |         <a data-sveltekit-preload-data="hover" href="{AppRoute.ALBUMS}/{album.id}" animate:flip={{ duration: 200 }}> | ||||||
|  |           <AlbumCard | ||||||
|  |             preload={index < 20} | ||||||
|  |             {album} | ||||||
|  |             on:showalbumcontextmenu={({ detail }) => showAlbumContextMenu(detail, album)} | ||||||
|  |           /> | ||||||
|  |         </a> | ||||||
|  |       {/each} | ||||||
|  |     </div> | ||||||
|  |   {:else if $albumViewSettings.view === AlbumViewMode.List} | ||||||
|  |     <AlbumsTable | ||||||
|  |       {sortByOptions} | ||||||
|  |       {albumsFiltered} | ||||||
|  |       onChooseAlbumToDelete={(album) => chooseAlbumToDelete(album)} | ||||||
|  |       onAlbumToEdit={(album) => handleEdit(album)} | ||||||
|  |     /> | ||||||
|  |   {/if} | ||||||
|  | 
 | ||||||
|  |   <!-- Empty Message --> | ||||||
|  | {:else} | ||||||
|  |   <EmptyPlaceholder text="Create an album to organize your photos and videos" onClick={handleCreateAlbum} /> | ||||||
|  | {/if} | ||||||
|  | 
 | ||||||
|  | <!-- Context Menu --> | ||||||
|  | {#if isShowContextMenu} | ||||||
|  |   <section class="fixed left-0 top-0 z-10 flex h-screen w-screen"> | ||||||
|  |     <ContextMenu {...contextMenuPosition} on:outclick={closeAlbumContextMenu} on:escape={closeAlbumContextMenu}> | ||||||
|  |       <MenuOption on:click={() => setAlbumToDelete()}> | ||||||
|  |         <span class="flex place-content-center place-items-center gap-2"> | ||||||
|  |           <Icon path={mdiDeleteOutline} size="18" /> | ||||||
|  |           <p>Delete album</p> | ||||||
|  |         </span> | ||||||
|  |       </MenuOption> | ||||||
|  |     </ContextMenu> | ||||||
|  |   </section> | ||||||
|  | {/if} | ||||||
|  | 
 | ||||||
|  | {#if albumToDelete} | ||||||
|  |   <ConfirmDialogue | ||||||
|  |     title="Delete Album" | ||||||
|  |     confirmText="Delete" | ||||||
|  |     onConfirm={deleteSelectedAlbum} | ||||||
|  |     onClose={() => (albumToDelete = null)} | ||||||
|  |   > | ||||||
|  |     <svelte:fragment slot="prompt"> | ||||||
|  |       <p>Are you sure you want to delete the album <b>{albumToDelete.albumName}</b>?</p> | ||||||
|  |       <p>If this album is shared, other users will not be able to access it anymore.</p> | ||||||
|  |     </svelte:fragment> | ||||||
|  |   </ConfirmDialogue> | ||||||
|  | {/if} | ||||||
| @ -1,14 +1,15 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import type { Sort } from '../../../routes/(user)/albums/+page.svelte'; |   import { albumViewSettings } from '$lib/stores/preferences.store'; | ||||||
|  |   import type { Sort } from '$lib/components/album-page/albums-list.svelte'; | ||||||
| 
 | 
 | ||||||
|   export let albumViewSettings: string; |  | ||||||
|   export let option: Sort; |   export let option: Sort; | ||||||
| 
 | 
 | ||||||
|   const handleSort = () => { |   const handleSort = () => { | ||||||
|     if (albumViewSettings === option.title) { |     if ($albumViewSettings.sortBy === option.title) { | ||||||
|  |       $albumViewSettings.sortDesc = !option.sortDesc; | ||||||
|       option.sortDesc = !option.sortDesc; |       option.sortDesc = !option.sortDesc; | ||||||
|     } else { |     } else { | ||||||
|       albumViewSettings = option.title; |       $albumViewSettings.sortBy = option.title; | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| </script> | </script> | ||||||
| @ -18,7 +19,7 @@ | |||||||
|     class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50" |     class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50" | ||||||
|     on:click={() => handleSort()} |     on:click={() => handleSort()} | ||||||
|   > |   > | ||||||
|     {#if albumViewSettings === option.title} |     {#if $albumViewSettings.sortBy === option.title} | ||||||
|       {#if option.sortDesc} |       {#if option.sortDesc} | ||||||
|         ↓ |         ↓ | ||||||
|       {:else} |       {:else} | ||||||
							
								
								
									
										87
									
								
								web/src/lib/components/album-page/albums-table.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								web/src/lib/components/album-page/albums-table.svelte
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import { AppRoute } from '$lib/constants'; | ||||||
|  |   import type { AlbumResponseDto } from '@immich/sdk'; | ||||||
|  |   import TableHeader from '$lib/components/album-page/albums-table-header.svelte'; | ||||||
|  |   import { goto } from '$app/navigation'; | ||||||
|  |   import Icon from '$lib/components/elements/icon.svelte'; | ||||||
|  |   import { mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; | ||||||
|  |   import type { Sort } from '$lib/components/album-page/albums-list.svelte'; | ||||||
|  |   import { locale } from '$lib/stores/preferences.store'; | ||||||
|  |   import { dateFormats } from '$lib/constants'; | ||||||
|  | 
 | ||||||
|  |   export let albumsFiltered: AlbumResponseDto[]; | ||||||
|  |   export let sortByOptions: Sort[]; | ||||||
|  |   export let onChooseAlbumToDelete: (album: AlbumResponseDto) => void; | ||||||
|  |   export let onAlbumToEdit: (album: AlbumResponseDto) => void; | ||||||
|  | 
 | ||||||
|  |   const dateLocaleString = (dateString: string) => { | ||||||
|  |     return new Date(dateString).toLocaleDateString($locale, dateFormats.album); | ||||||
|  |   }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <table class="mt-2 w-full text-left"> | ||||||
|  |   <thead | ||||||
|  |     class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary" | ||||||
|  |   > | ||||||
|  |     <tr class="flex w-full place-items-center p-2 md:p-5"> | ||||||
|  |       {#each sortByOptions as option, index (index)} | ||||||
|  |         <TableHeader {option} /> | ||||||
|  |       {/each} | ||||||
|  |       <th class="hidden text-center text-sm font-medium 2xl:block 2xl:w-[12%]">Action</th> | ||||||
|  |     </tr> | ||||||
|  |   </thead> | ||||||
|  |   <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg"> | ||||||
|  |     {#each albumsFiltered as album (album.id)} | ||||||
|  |       <tr | ||||||
|  |         class="flex h-[50px] w-full place-items-center border-[3px] border-transparent p-2 text-center odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5" | ||||||
|  |         on:click={() => goto(`${AppRoute.ALBUMS}/${album.id}`)} | ||||||
|  |         on:keydown={(event) => event.key === 'Enter' && goto(`${AppRoute.ALBUMS}/${album.id}`)} | ||||||
|  |         tabindex="0" | ||||||
|  |       > | ||||||
|  |         <a data-sveltekit-preload-data="hover" class="flex w-full" href="{AppRoute.ALBUMS}/{album.id}"> | ||||||
|  |           <td class="text-md text-ellipsis text-left w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]" | ||||||
|  |             >{album.albumName}</td | ||||||
|  |           > | ||||||
|  |           <td class="text-md text-ellipsis text-center sm:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]"> | ||||||
|  |             {album.assetCount} | ||||||
|  |             {album.assetCount > 1 ? `items` : `item`} | ||||||
|  |           </td> | ||||||
|  |           <td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]" | ||||||
|  |             >{dateLocaleString(album.updatedAt)} | ||||||
|  |           </td> | ||||||
|  |           <td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]" | ||||||
|  |             >{dateLocaleString(album.createdAt)}</td | ||||||
|  |           > | ||||||
|  |           <td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]"> | ||||||
|  |             {#if album.endDate} | ||||||
|  |               {dateLocaleString(album.endDate)} | ||||||
|  |             {:else} | ||||||
|  |               ❌ | ||||||
|  |             {/if}</td | ||||||
|  |           > | ||||||
|  |           <td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]" | ||||||
|  |             >{#if album.startDate} | ||||||
|  |               {dateLocaleString(album.startDate)} | ||||||
|  |             {:else} | ||||||
|  |               ❌ | ||||||
|  |             {/if}</td | ||||||
|  |           > | ||||||
|  |         </a> | ||||||
|  |         <td class="text-md hidden text-ellipsis text-center 2xl:block xl:w-[15%] 2xl:w-[12%]"> | ||||||
|  |           <button | ||||||
|  |             on:click|stopPropagation={() => onAlbumToEdit(album)} | ||||||
|  |             class="rounded-full z-1 bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700" | ||||||
|  |           > | ||||||
|  |             <Icon path={mdiPencilOutline} size="16" /> | ||||||
|  |           </button> | ||||||
|  |           <button | ||||||
|  |             on:click|stopPropagation={() => onChooseAlbumToDelete(album)} | ||||||
|  |             class="rounded-full z-1 bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700" | ||||||
|  |           > | ||||||
|  |             <Icon path={mdiTrashCanOutline} size="16" /> | ||||||
|  |           </button> | ||||||
|  |         </td> | ||||||
|  |       </tr> | ||||||
|  |     {/each} | ||||||
|  |   </tbody> | ||||||
|  | </table> | ||||||
| @ -1,406 +1,17 @@ | |||||||
| <script lang="ts" context="module"> |  | ||||||
|   export interface Sort { |  | ||||||
|     title: string; |  | ||||||
|     sortDesc: boolean; |  | ||||||
|     widthClass: string; |  | ||||||
|     sortFn: (reverse: boolean, albums: AlbumResponseDto[]) => AlbumResponseDto[]; |  | ||||||
|   } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import { goto } from '$app/navigation'; |  | ||||||
|   import AlbumCard from '$lib/components/album-page/album-card.svelte'; |  | ||||||
|   import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; |  | ||||||
|   import Dropdown from '$lib/components/elements/dropdown.svelte'; |  | ||||||
|   import Icon from '$lib/components/elements/icon.svelte'; |  | ||||||
|   import TableHeader from '$lib/components/elements/table-header.svelte'; |  | ||||||
|   import EditAlbumForm from '$lib/components/forms/edit-album-form.svelte'; |  | ||||||
|   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; |   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; | ||||||
|   import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.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 EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; |  | ||||||
|   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; |  | ||||||
|   import { |  | ||||||
|     NotificationType, |  | ||||||
|     notificationController, |  | ||||||
|   } from '$lib/components/shared-components/notification/notification'; |  | ||||||
|   import { AppRoute, dateFormats } from '$lib/constants'; |  | ||||||
|   import { AlbumViewMode, albumViewSettings, locale } from '$lib/stores/preferences.store'; |  | ||||||
|   import type { AlbumResponseDto } from '@immich/sdk'; |  | ||||||
|   import { |  | ||||||
|     mdiArrowDownThin, |  | ||||||
|     mdiArrowUpThin, |  | ||||||
|     mdiDeleteOutline, |  | ||||||
|     mdiFormatListBulletedSquare, |  | ||||||
|     mdiPencilOutline, |  | ||||||
|     mdiPlusBoxOutline, |  | ||||||
|     mdiTrashCanOutline, |  | ||||||
|     mdiViewGridOutline, |  | ||||||
|   } from '@mdi/js'; |  | ||||||
|   import { orderBy } from 'lodash-es'; |  | ||||||
|   import { onMount } from 'svelte'; |  | ||||||
|   import { flip } from 'svelte/animate'; |  | ||||||
|   import type { PageData } from './$types'; |   import type { PageData } from './$types'; | ||||||
|   import { useAlbums } from './albums.bloc'; |   import AlbumsControls from '$lib/components/album-page/albums-controls.svelte'; | ||||||
|   import SearchBar from '$lib/components/elements/search-bar.svelte'; |   import Albums from '$lib/components/album-page/albums-list.svelte'; | ||||||
| 
 | 
 | ||||||
|   export let data: PageData; |   export let data: PageData; | ||||||
| 
 | 
 | ||||||
|   let shouldShowEditUserForm = false; |  | ||||||
|   let selectedAlbum: AlbumResponseDto; |  | ||||||
|   let searchAlbum = ''; |   let searchAlbum = ''; | ||||||
| 
 |  | ||||||
|   let sortByOptions: Record<string, Sort> = { |  | ||||||
|     albumTitle: { |  | ||||||
|       title: 'Album title', |  | ||||||
|       sortDesc: $albumViewSettings.sortDesc, // Load Sort Direction |  | ||||||
|       widthClass: 'text-left w-8/12 sm:w-4/12 md:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]', |  | ||||||
|       sortFn: (reverse, albums) => { |  | ||||||
|         return orderBy(albums, 'albumName', [reverse ? 'desc' : 'asc']); |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     numberOfAssets: { |  | ||||||
|       title: 'Number of assets', |  | ||||||
|       sortDesc: $albumViewSettings.sortDesc, |  | ||||||
|       widthClass: 'text-center w-4/12 m:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]', |  | ||||||
|       sortFn: (reverse, albums) => { |  | ||||||
|         return orderBy(albums, 'assetCount', [reverse ? 'desc' : 'asc']); |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     lastModified: { |  | ||||||
|       title: 'Last modified', |  | ||||||
|       sortDesc: $albumViewSettings.sortDesc, |  | ||||||
|       widthClass: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]', |  | ||||||
|       sortFn: (reverse, albums) => { |  | ||||||
|         return orderBy(albums, [(album) => new Date(album.updatedAt)], [reverse ? 'desc' : 'asc']); |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     created: { |  | ||||||
|       title: 'Created date', |  | ||||||
|       sortDesc: $albumViewSettings.sortDesc, |  | ||||||
|       widthClass: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]', |  | ||||||
|       sortFn: (reverse, albums) => { |  | ||||||
|         return orderBy(albums, [(album) => new Date(album.createdAt)], [reverse ? 'desc' : 'asc']); |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     mostRecent: { |  | ||||||
|       title: 'Most recent photo', |  | ||||||
|       sortDesc: $albumViewSettings.sortDesc, |  | ||||||
|       widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]', |  | ||||||
|       sortFn: (reverse, albums) => { |  | ||||||
|         return orderBy( |  | ||||||
|           albums, |  | ||||||
|           [(album) => (album.endDate ? new Date(album.endDate) : '')], |  | ||||||
|           [reverse ? 'desc' : 'asc'], |  | ||||||
|         ).sort((a, b) => { |  | ||||||
|           if (a.endDate === undefined) { |  | ||||||
|             return 1; |  | ||||||
|           } |  | ||||||
|           if (b.endDate === undefined) { |  | ||||||
|             return -1; |  | ||||||
|           } |  | ||||||
|           return 0; |  | ||||||
|         }); |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     mostOld: { |  | ||||||
|       title: 'Oldest photo', |  | ||||||
|       sortDesc: $albumViewSettings.sortDesc, |  | ||||||
|       widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]', |  | ||||||
|       sortFn: (reverse, albums) => { |  | ||||||
|         return orderBy( |  | ||||||
|           albums, |  | ||||||
|           [(album) => (album.startDate ? new Date(album.startDate) : null)], |  | ||||||
|           [reverse ? 'desc' : 'asc'], |  | ||||||
|         ).sort((a, b) => { |  | ||||||
|           if (a.startDate === undefined) { |  | ||||||
|             return 1; |  | ||||||
|           } |  | ||||||
|           if (b.startDate === undefined) { |  | ||||||
|             return -1; |  | ||||||
|           } |  | ||||||
|           return 0; |  | ||||||
|         }); |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleEdit = (album: AlbumResponseDto) => { |  | ||||||
|     selectedAlbum = { ...album }; |  | ||||||
|     shouldShowEditUserForm = true; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const { |  | ||||||
|     albums: unsortedAlbums, |  | ||||||
|     isShowContextMenu, |  | ||||||
|     contextMenuPosition, |  | ||||||
|     contextMenuTargetAlbum, |  | ||||||
|     createAlbum, |  | ||||||
|     deleteAlbum, |  | ||||||
|     showAlbumContextMenu, |  | ||||||
|     closeAlbumContextMenu, |  | ||||||
|   } = useAlbums({ albums: data.albums }); |  | ||||||
| 
 |  | ||||||
|   let albums = unsortedAlbums; |  | ||||||
|   let albumToDelete: AlbumResponseDto | null; |  | ||||||
| 
 |  | ||||||
|   const chooseAlbumToDelete = (album: AlbumResponseDto) => { |  | ||||||
|     $contextMenuTargetAlbum = album; |  | ||||||
|     setAlbumToDelete(); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const setAlbumToDelete = () => { |  | ||||||
|     albumToDelete = $contextMenuTargetAlbum ?? null; |  | ||||||
|     closeAlbumContextMenu(); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const deleteSelectedAlbum = async () => { |  | ||||||
|     if (!albumToDelete) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     try { |  | ||||||
|       await deleteAlbum(albumToDelete); |  | ||||||
|     } catch { |  | ||||||
|       notificationController.show({ |  | ||||||
|         message: 'Error deleting album', |  | ||||||
|         type: NotificationType.Error, |  | ||||||
|       }); |  | ||||||
|     } finally { |  | ||||||
|       albumToDelete = null; |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   $: { |  | ||||||
|     for (const key in sortByOptions) { |  | ||||||
|       if (sortByOptions[key].title === $albumViewSettings.sortBy) { |  | ||||||
|         $albums = sortByOptions[key].sortFn(sortByOptions[key].sortDesc, $unsortedAlbums); |  | ||||||
|         $albumViewSettings.sortDesc = sortByOptions[key].sortDesc; // "Save" sortDesc |  | ||||||
|         $albumViewSettings.sortBy = sortByOptions[key].title; |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   $: albumsFiltered = $albums.filter((album) => album.albumName.toLowerCase().includes(searchAlbum.toLowerCase())); |  | ||||||
| 
 |  | ||||||
|   const searchSort = (searched: string): Sort => { |  | ||||||
|     for (const key in sortByOptions) { |  | ||||||
|       if (sortByOptions[key].title === searched) { |  | ||||||
|         return sortByOptions[key]; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return sortByOptions[0]; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleCreateAlbum = async () => { |  | ||||||
|     const newAlbum = await createAlbum(); |  | ||||||
|     if (newAlbum) { |  | ||||||
|       await goto(`${AppRoute.ALBUMS}/${newAlbum.id}`); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const dateLocaleString = (dateString: string) => { |  | ||||||
|     return new Date(dateString).toLocaleDateString($locale, dateFormats.album); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   onMount(async () => { |  | ||||||
|     await removeAlbumsIfEmpty(); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   const removeAlbumsIfEmpty = async () => { |  | ||||||
|     try { |  | ||||||
|       for (const album of $albums) { |  | ||||||
|         if (album.assetCount == 0 && album.albumName == '') { |  | ||||||
|           await deleteAlbum(album); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } catch (error) { |  | ||||||
|       console.log(error); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const successModifyAlbum = () => { |  | ||||||
|     shouldShowEditUserForm = false; |  | ||||||
|     notificationController.show({ |  | ||||||
|       message: 'Album infos updated', |  | ||||||
|       type: NotificationType.Info, |  | ||||||
|     }); |  | ||||||
|     $albums[$albums.findIndex((x) => x.id === selectedAlbum.id)] = selectedAlbum; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleChangeListMode = () => { |  | ||||||
|     $albumViewSettings.view = |  | ||||||
|       $albumViewSettings.view === AlbumViewMode.Cover ? AlbumViewMode.List : AlbumViewMode.Cover; |  | ||||||
|   }; |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if shouldShowEditUserForm} |  | ||||||
|   <FullScreenModal onClose={() => (shouldShowEditUserForm = false)}> |  | ||||||
|     <EditAlbumForm |  | ||||||
|       album={selectedAlbum} |  | ||||||
|       on:editSuccess={() => successModifyAlbum()} |  | ||||||
|       on:cancel={() => (shouldShowEditUserForm = false)} |  | ||||||
|     /> |  | ||||||
|   </FullScreenModal> |  | ||||||
| {/if} |  | ||||||
| 
 |  | ||||||
| <UserPageLayout title={data.meta.title}> | <UserPageLayout title={data.meta.title}> | ||||||
|   <div class="flex place-items-center gap-2" slot="buttons"> |   <div class="flex place-items-center gap-2" slot="buttons"> | ||||||
|     <div class="hidden lg:block lg:w-40 xl:w-60 2xl:w-80 h-10"> |     <AlbumsControls bind:searchAlbum /> | ||||||
|       <SearchBar placeholder="Search albums" bind:name={searchAlbum} isSearching={false} /> |  | ||||||
|   </div> |   </div> | ||||||
|     <LinkButton on:click={handleCreateAlbum}> |   <Albums albums={data.albums} {searchAlbum} /> | ||||||
|       <div class="flex place-items-center gap-2 text-sm"> |  | ||||||
|         <Icon path={mdiPlusBoxOutline} size="18" /> |  | ||||||
|         Create album |  | ||||||
|       </div> |  | ||||||
|     </LinkButton> |  | ||||||
| 
 |  | ||||||
|     <Dropdown |  | ||||||
|       options={Object.values(sortByOptions)} |  | ||||||
|       selectedOption={searchSort($albumViewSettings.sortBy)} |  | ||||||
|       render={(option) => { |  | ||||||
|         return { |  | ||||||
|           title: option.title, |  | ||||||
|           icon: option.sortDesc ? mdiArrowDownThin : mdiArrowUpThin, |  | ||||||
|         }; |  | ||||||
|       }} |  | ||||||
|       on:select={(event) => { |  | ||||||
|         for (const key in sortByOptions) { |  | ||||||
|           if (sortByOptions[key].title === event.detail.title) { |  | ||||||
|             sortByOptions[key].sortDesc = !sortByOptions[key].sortDesc; |  | ||||||
|             $albumViewSettings.sortBy = sortByOptions[key].title; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       }} |  | ||||||
|     /> |  | ||||||
| 
 |  | ||||||
|     <LinkButton on:click={() => handleChangeListMode()}> |  | ||||||
|       <div class="flex place-items-center gap-2 text-sm"> |  | ||||||
|         {#if $albumViewSettings.view === AlbumViewMode.List} |  | ||||||
|           <Icon path={mdiViewGridOutline} size="18" /> |  | ||||||
|           <p class="hidden sm:block">Cover</p> |  | ||||||
|         {:else} |  | ||||||
|           <Icon path={mdiFormatListBulletedSquare} size="18" /> |  | ||||||
|           <p class="hidden sm:block">List</p> |  | ||||||
|         {/if} |  | ||||||
|       </div> |  | ||||||
|     </LinkButton> |  | ||||||
|   </div> |  | ||||||
|   {#if $albums.length > 0} |  | ||||||
|     <!-- Album Card --> |  | ||||||
|     {#if $albumViewSettings.view === AlbumViewMode.Cover} |  | ||||||
|       <div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]"> |  | ||||||
|         {#each albumsFiltered as album, index (album.id)} |  | ||||||
|           <a data-sveltekit-preload-data="hover" href="{AppRoute.ALBUMS}/{album.id}" animate:flip={{ duration: 200 }}> |  | ||||||
|             <AlbumCard |  | ||||||
|               preload={index < 20} |  | ||||||
|               {album} |  | ||||||
|               on:showalbumcontextmenu={(e) => showAlbumContextMenu(e.detail, album)} |  | ||||||
|             /> |  | ||||||
|           </a> |  | ||||||
|         {/each} |  | ||||||
|       </div> |  | ||||||
|     {:else if $albumViewSettings.view === AlbumViewMode.List} |  | ||||||
|       <table class="mt-2 w-full text-left"> |  | ||||||
|         <thead |  | ||||||
|           class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary" |  | ||||||
|         > |  | ||||||
|           <tr class="flex w-full place-items-center p-2 md:p-5"> |  | ||||||
|             {#each Object.keys(sortByOptions) as key (key)} |  | ||||||
|               <TableHeader bind:albumViewSettings={$albumViewSettings.sortBy} bind:option={sortByOptions[key]} /> |  | ||||||
|             {/each} |  | ||||||
|             <th class="hidden text-center text-sm font-medium 2xl:block 2xl:w-[12%]">Action</th> |  | ||||||
|           </tr> |  | ||||||
|         </thead> |  | ||||||
|         <tbody |  | ||||||
|           class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg" |  | ||||||
|         > |  | ||||||
|           {#each albumsFiltered as album (album.id)} |  | ||||||
|             <tr |  | ||||||
|               class="flex h-[50px] w-full place-items-center border-[3px] border-transparent p-2 text-center odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5" |  | ||||||
|               on:click={() => goto(`${AppRoute.ALBUMS}/${album.id}`)} |  | ||||||
|               on:keydown={(event) => event.key === 'Enter' && goto(`${AppRoute.ALBUMS}/${album.id}`)} |  | ||||||
|               tabindex="0" |  | ||||||
|             > |  | ||||||
|               <a data-sveltekit-preload-data="hover" class="flex w-full" href="{AppRoute.ALBUMS}/{album.id}"> |  | ||||||
|                 <td class="text-md text-ellipsis text-left w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]" |  | ||||||
|                   >{album.albumName}</td |  | ||||||
|                 > |  | ||||||
|                 <td class="text-md text-ellipsis text-center sm:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]"> |  | ||||||
|                   {album.assetCount} |  | ||||||
|                   {album.assetCount > 1 ? `items` : `item`} |  | ||||||
|                 </td> |  | ||||||
|                 <td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]" |  | ||||||
|                   >{dateLocaleString(album.updatedAt)} |  | ||||||
|                 </td> |  | ||||||
|                 <td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]" |  | ||||||
|                   >{dateLocaleString(album.createdAt)}</td |  | ||||||
|                 > |  | ||||||
|                 <td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]"> |  | ||||||
|                   {#if album.endDate} |  | ||||||
|                     {dateLocaleString(album.endDate)} |  | ||||||
|                   {:else} |  | ||||||
|                     ❌ |  | ||||||
|                   {/if}</td |  | ||||||
|                 > |  | ||||||
|                 <td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]" |  | ||||||
|                   >{#if album.startDate} |  | ||||||
|                     {dateLocaleString(album.startDate)} |  | ||||||
|                   {:else} |  | ||||||
|                     ❌ |  | ||||||
|                   {/if}</td |  | ||||||
|                 > |  | ||||||
|               </a> |  | ||||||
|               <td class="text-md hidden text-ellipsis text-center 2xl:block xl:w-[15%] 2xl:w-[12%]"> |  | ||||||
|                 <button |  | ||||||
|                   on:click|stopPropagation={() => handleEdit(album)} |  | ||||||
|                   class="rounded-full z-1 bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700" |  | ||||||
|                 > |  | ||||||
|                   <Icon path={mdiPencilOutline} size="16" /> |  | ||||||
|                 </button> |  | ||||||
|                 <button |  | ||||||
|                   on:click|stopPropagation={() => chooseAlbumToDelete(album)} |  | ||||||
|                   class="rounded-full z-1 bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700" |  | ||||||
|                 > |  | ||||||
|                   <Icon path={mdiTrashCanOutline} size="16" /> |  | ||||||
|                 </button> |  | ||||||
|               </td> |  | ||||||
|             </tr> |  | ||||||
|           {/each} |  | ||||||
|         </tbody> |  | ||||||
|       </table> |  | ||||||
|     {/if} |  | ||||||
| 
 |  | ||||||
|     <!-- Empty Message --> |  | ||||||
|   {:else} |  | ||||||
|     <EmptyPlaceholder text="Create an album to organize your photos and videos" onClick={handleCreateAlbum} /> |  | ||||||
|   {/if} |  | ||||||
| </UserPageLayout> | </UserPageLayout> | ||||||
| 
 |  | ||||||
| <!-- Context Menu --> |  | ||||||
| {#if $isShowContextMenu} |  | ||||||
|   <ContextMenu {...$contextMenuPosition} on:outclick={closeAlbumContextMenu} on:escape={closeAlbumContextMenu}> |  | ||||||
|     <MenuOption on:click={() => setAlbumToDelete()}> |  | ||||||
|       <span class="flex place-content-center place-items-center gap-2"> |  | ||||||
|         <Icon path={mdiDeleteOutline} size="18" /> |  | ||||||
|         <p>Delete album</p> |  | ||||||
|       </span> |  | ||||||
|     </MenuOption> |  | ||||||
|   </ContextMenu> |  | ||||||
| {/if} |  | ||||||
| 
 |  | ||||||
| {#if albumToDelete} |  | ||||||
|   <ConfirmDialogue |  | ||||||
|     title="Delete Album" |  | ||||||
|     confirmText="Delete" |  | ||||||
|     onConfirm={deleteSelectedAlbum} |  | ||||||
|     onClose={() => (albumToDelete = null)} |  | ||||||
|   > |  | ||||||
|     <svelte:fragment slot="prompt"> |  | ||||||
|       <p>Are you sure you want to delete the album <b>{albumToDelete.albumName}</b>?</p> |  | ||||||
|       <p>If this album is shared, other users will not be able to access it anymore.</p> |  | ||||||
|     </svelte:fragment> |  | ||||||
|   </ConfirmDialogue> |  | ||||||
| {/if} |  | ||||||
|  | |||||||
| @ -1,73 +0,0 @@ | |||||||
| import type { OnShowContextMenuDetail } from '$lib/components/album-page/album-card'; |  | ||||||
| import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; |  | ||||||
| import { asyncTimeout } from '$lib/utils'; |  | ||||||
| import { handleError } from '$lib/utils/handle-error'; |  | ||||||
| import { createAlbum, deleteAlbum, getAllAlbums, type AlbumResponseDto } from '@immich/sdk'; |  | ||||||
| import { derived, get, writable } from 'svelte/store'; |  | ||||||
| 
 |  | ||||||
| type AlbumsProperties = { albums: AlbumResponseDto[] }; |  | ||||||
| 
 |  | ||||||
| export const useAlbums = (properties: AlbumsProperties) => { |  | ||||||
|   const albums = writable([...properties.albums]); |  | ||||||
|   const contextMenuPosition = writable<OnShowContextMenuDetail>({ x: 0, y: 0 }); |  | ||||||
|   const contextMenuTargetAlbum = writable<AlbumResponseDto | undefined>(); |  | ||||||
|   const isShowContextMenu = derived(contextMenuTargetAlbum, ($selectedAlbum) => !!$selectedAlbum); |  | ||||||
| 
 |  | ||||||
|   async function loadAlbums(): Promise<void> { |  | ||||||
|     try { |  | ||||||
|       const data = await getAllAlbums({}); |  | ||||||
|       albums.set(data); |  | ||||||
| 
 |  | ||||||
|       // Delete album that has no photos and is named ''
 |  | ||||||
|       for (const album of data) { |  | ||||||
|         if (album.albumName === '' && album.assetCount === 0) { |  | ||||||
|           await asyncTimeout(500); |  | ||||||
|           await handleDeleteAlbum(album); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } catch { |  | ||||||
|       notificationController.show({ |  | ||||||
|         message: 'Error loading albums', |  | ||||||
|         type: NotificationType.Error, |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function handleCreateAlbum(): Promise<AlbumResponseDto | undefined> { |  | ||||||
|     try { |  | ||||||
|       return await createAlbum({ createAlbumDto: { albumName: '' } }); |  | ||||||
|     } catch (error) { |  | ||||||
|       handleError(error, 'Unable to create album'); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function handleDeleteAlbum(albumToDelete: AlbumResponseDto): Promise<void> { |  | ||||||
|     await deleteAlbum({ id: albumToDelete.id }); |  | ||||||
|     albums.set(get(albums).filter(({ id }) => id !== albumToDelete.id)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function showAlbumContextMenu(contextMenuDetail: OnShowContextMenuDetail, album: AlbumResponseDto): void { |  | ||||||
|     contextMenuTargetAlbum.set(album); |  | ||||||
| 
 |  | ||||||
|     contextMenuPosition.set({ |  | ||||||
|       x: contextMenuDetail.x, |  | ||||||
|       y: contextMenuDetail.y, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function closeAlbumContextMenu() { |  | ||||||
|     contextMenuTargetAlbum.set(undefined); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     albums, |  | ||||||
|     isShowContextMenu, |  | ||||||
|     contextMenuPosition, |  | ||||||
|     contextMenuTargetAlbum, |  | ||||||
|     loadAlbums, |  | ||||||
|     createAlbum: handleCreateAlbum, |  | ||||||
|     deleteAlbum: handleDeleteAlbum, |  | ||||||
|     showAlbumContextMenu, |  | ||||||
|     closeAlbumContextMenu, |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user