mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(web): add better face management UI action (#3328)
* add better face management menu * context menu * change name form * change name * navigate to merge face * fix web
This commit is contained in:
		
							
								
								
									
										64
									
								
								web/src/lib/components/faces-page/people-card.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								web/src/lib/components/faces-page/people-card.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import { PersonResponseDto, api } from '@api'; | ||||||
|  |   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; | ||||||
|  |   import IconButton from '../elements/buttons/icon-button.svelte'; | ||||||
|  |   import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; | ||||||
|  |   import ContextMenu from '../shared-components/context-menu/context-menu.svelte'; | ||||||
|  |   import MenuOption from '../shared-components/context-menu/menu-option.svelte'; | ||||||
|  |   import Portal from '../shared-components/portal/portal.svelte'; | ||||||
|  |   import { createEventDispatcher } from 'svelte'; | ||||||
|  |  | ||||||
|  |   export let person: PersonResponseDto; | ||||||
|  |  | ||||||
|  |   let showContextMenu = false; | ||||||
|  |   let dispatch = createEventDispatcher(); | ||||||
|  |  | ||||||
|  |   const onChangeNameClicked = () => { | ||||||
|  |     dispatch('change-name', person); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const onMergeFacesClicked = () => { | ||||||
|  |     dispatch('merge-faces', person); | ||||||
|  |   }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <div id="people-card" class="relative"> | ||||||
|  |   <a href="/people/{person.id}" draggable="false"> | ||||||
|  |     <div class="filter brightness-95 rounded-xl w-48"> | ||||||
|  |       <ImageThumbnail shadow url={api.getPeopleThumbnailUrl(person.id)} altText={person.name} widthStyle="100%" /> | ||||||
|  |     </div> | ||||||
|  |     {#if person.name} | ||||||
|  |       <span | ||||||
|  |         class="absolute bottom-2 w-full text-center font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]" | ||||||
|  |       > | ||||||
|  |         {person.name} | ||||||
|  |       </span> | ||||||
|  |     {/if} | ||||||
|  |   </a> | ||||||
|  |  | ||||||
|  |   <button | ||||||
|  |     class="absolute top-2 right-2 z-20" | ||||||
|  |     on:click|stopPropagation|preventDefault={() => { | ||||||
|  |       showContextMenu = !showContextMenu; | ||||||
|  |     }} | ||||||
|  |     data-testid="context-button-parent" | ||||||
|  |     id={`icon-${person.id}`} | ||||||
|  |   > | ||||||
|  |     <IconButton color="transparent-primary"> | ||||||
|  |       <DotsVertical size="20" /> | ||||||
|  |     </IconButton> | ||||||
|  |  | ||||||
|  |     {#if showContextMenu} | ||||||
|  |       <ContextMenu on:outclick={() => (showContextMenu = false)}> | ||||||
|  |         <MenuOption on:click={() => onChangeNameClicked()} text="Change name" /> | ||||||
|  |         <MenuOption on:click={() => onMergeFacesClicked()} text="Merge faces" /> | ||||||
|  |       </ContextMenu> | ||||||
|  |     {/if} | ||||||
|  |   </button> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | {#if showContextMenu} | ||||||
|  |   <Portal target="body"> | ||||||
|  |     <div class="absolute top-0 left-0 heyo w-screen h-screen bg-transparent z-10" /> | ||||||
|  |   </Portal> | ||||||
|  | {/if} | ||||||
| @@ -1,46 +1,112 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; |  | ||||||
|   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; |   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; | ||||||
|   import { api } from '@api'; |  | ||||||
|   import AccountOff from 'svelte-material-icons/AccountOff.svelte'; |   import AccountOff from 'svelte-material-icons/AccountOff.svelte'; | ||||||
|   import type { PageData } from './$types'; |   import type { PageData } from './$types'; | ||||||
|  |   import PeopleCard from '$lib/components/faces-page/people-card.svelte'; | ||||||
|  |   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; | ||||||
|  |   import Button from '$lib/components/elements/buttons/button.svelte'; | ||||||
|  |   import { api, type PersonResponseDto } from '@api'; | ||||||
|  |   import { handleError } from '$lib/utils/handle-error'; | ||||||
|  |   import { | ||||||
|  |     notificationController, | ||||||
|  |     NotificationType, | ||||||
|  |   } from '$lib/components/shared-components/notification/notification'; | ||||||
|  |   import { goto } from '$app/navigation'; | ||||||
|  |   import { AppRoute } from '$lib/constants'; | ||||||
|   export let data: PageData; |   export let data: PageData; | ||||||
|  |  | ||||||
|  |   let showChangeNameModal = false; | ||||||
|  |   let personName = ''; | ||||||
|  |   let edittingPerson: PersonResponseDto | null = null; | ||||||
|  |  | ||||||
|  |   const handleChangeName = ({ detail }: CustomEvent<PersonResponseDto>) => { | ||||||
|  |     showChangeNameModal = true; | ||||||
|  |     personName = detail.name; | ||||||
|  |     edittingPerson = detail; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const handleMergeFaces = (event: CustomEvent<PersonResponseDto>) => { | ||||||
|  |     goto(`${AppRoute.PEOPLE}/${event.detail.id}?action=merge`); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const submitNameChange = async () => { | ||||||
|  |     try { | ||||||
|  |       if (edittingPerson) { | ||||||
|  |         const { data: updatedPerson } = await api.personApi.updatePerson({ | ||||||
|  |           id: edittingPerson.id, | ||||||
|  |           personUpdateDto: { name: personName }, | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         data.people = data.people.map((person: PersonResponseDto) => { | ||||||
|  |           if (person.id === updatedPerson.id) { | ||||||
|  |             return updatedPerson; | ||||||
|  |           } | ||||||
|  |           return person; | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         showChangeNameModal = false; | ||||||
|  |  | ||||||
|  |         notificationController.show({ | ||||||
|  |           message: 'Change name succesfully', | ||||||
|  |           type: NotificationType.Info, | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       handleError(error, 'Unable to save name'); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <UserPageLayout user={data.user} showUploadButton title="People"> | <UserPageLayout user={data.user} showUploadButton title="People"> | ||||||
|   {#if data.people.length > 0} |   <section> | ||||||
|     <div class="pl-4"> |     {#if data.people.length > 0} | ||||||
|       <div class="flex flex-row flex-wrap gap-1"> |       <div class="pl-4"> | ||||||
|         {#each data.people as person (person.id)} |         <div class="flex flex-row flex-wrap gap-1"> | ||||||
|           <div class="relative"> |           {#each data.people as person (person.id)} | ||||||
|             <a href="/people/{person.id}" draggable="false"> |             <PeopleCard {person} on:change-name={handleChangeName} on:merge-faces={handleMergeFaces} /> | ||||||
|               <div class="filter brightness-95 rounded-xl w-48"> |           {/each} | ||||||
|                 <ImageThumbnail |         </div> | ||||||
|                   shadow |       </div> | ||||||
|                   url={api.getPeopleThumbnailUrl(person.id)} |     {:else} | ||||||
|                   altText={person.name} |       <div class="flex items-center place-content-center w-full min-h-[calc(66vh_-_11rem)] dark:text-white"> | ||||||
|                   widthStyle="100%" |         <div class="flex flex-col content-center items-center text-center"> | ||||||
|                 /> |           <AccountOff size="3.5em" /> | ||||||
|               </div> |           <p class="font-medium text-3xl mt-5">No people</p> | ||||||
|               {#if person.name} |         </div> | ||||||
|                 <span |       </div> | ||||||
|                   class="absolute bottom-2 w-full text-center font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]" |     {/if} | ||||||
|                 > |   </section> | ||||||
|                   {person.name} |  | ||||||
|                 </span> |   {#if showChangeNameModal} | ||||||
|               {/if} |     <FullScreenModal on:clickOutside={() => (showChangeNameModal = false)}> | ||||||
|             </a> |       <div | ||||||
|  |         class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-3xl py-8 dark:text-immich-dark-fg" | ||||||
|  |       > | ||||||
|  |         <div | ||||||
|  |           class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary" | ||||||
|  |         > | ||||||
|  |           <h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">Change name</h1> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <form on:submit|preventDefault={submitNameChange} autocomplete="off"> | ||||||
|  |           <div class="m-4 flex flex-col gap-2"> | ||||||
|  |             <label class="immich-form-label" for="email">Name</label> | ||||||
|  |             <!-- svelte-ignore a11y-autofocus --> | ||||||
|  |             <input class="immich-form-input" id="name" name="name" type="text" bind:value={personName} autofocus /> | ||||||
|           </div> |           </div> | ||||||
|         {/each} |  | ||||||
|  |           <div class="flex w-full px-4 gap-4 mt-8"> | ||||||
|  |             <Button | ||||||
|  |               color="gray" | ||||||
|  |               fullwidth | ||||||
|  |               on:click={() => { | ||||||
|  |                 showChangeNameModal = false; | ||||||
|  |               }}>Cancel</Button | ||||||
|  |             > | ||||||
|  |             <Button type="submit" fullwidth>Ok</Button> | ||||||
|  |           </div> | ||||||
|  |         </form> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </FullScreenModal> | ||||||
|   {:else} |  | ||||||
|     <div class="flex items-center place-content-center w-full min-h-[calc(66vh_-_11rem)] dark:text-white"> |  | ||||||
|       <div class="flex flex-col content-center items-center text-center"> |  | ||||||
|         <AccountOff size="3.5em" /> |  | ||||||
|         <p class="font-medium text-3xl mt-5">No people</p> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   {/if} |   {/if} | ||||||
| </UserPageLayout> | </UserPageLayout> | ||||||
|   | |||||||
| @@ -29,6 +29,7 @@ | |||||||
|     notificationController, |     notificationController, | ||||||
|   } from '$lib/components/shared-components/notification/notification'; |   } from '$lib/components/shared-components/notification/notification'; | ||||||
|   import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte'; |   import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte'; | ||||||
|  |   import { onMount } from 'svelte'; | ||||||
|  |  | ||||||
|   export let data: PageData; |   export let data: PageData; | ||||||
|   let isEditingName = false; |   let isEditingName = false; | ||||||
| @@ -42,6 +43,12 @@ | |||||||
|  |  | ||||||
|   $: showAssets = !showMergeFacePanel && !showFaceThumbnailSelection; |   $: showAssets = !showMergeFacePanel && !showFaceThumbnailSelection; | ||||||
|  |  | ||||||
|  |   onMount(() => { | ||||||
|  |     const action = $page.url.searchParams.get('action'); | ||||||
|  |     if (action == 'merge') { | ||||||
|  |       showMergeFacePanel = true; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|   afterNavigate(({ from }) => { |   afterNavigate(({ from }) => { | ||||||
|     // Prevent setting previousRoute to the current page. |     // Prevent setting previousRoute to the current page. | ||||||
|     if (from && from.route.id !== $page.route.id) { |     if (from && from.route.id !== $page.route.id) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user