Explorar o código

feat(web): suggest people when typing a name (#4126)

* feat(web): suggest people when entering a name

* fix: border size from 2 to 1 pixel

* pr feedback

* fix: web unit test

* pr feedback

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
martin hai 1 ano
pai
achega
fc64be6603

+ 5 - 5
web/src/lib/components/faces-page/edit-name-input.svelte

@@ -3,10 +3,10 @@
   import { createEventDispatcher } from 'svelte';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
   import Button from '../elements/buttons/button.svelte';
-  import { clickOutside } from '$lib/utils/click-outside';
 
   export let person: PersonResponseDto;
-  let name = person.name;
+  export let name: string;
+  export let suggestedPeople = false;
 
   const dispatch = createEventDispatcher<{
     change: string;
@@ -15,9 +15,9 @@
 </script>
 
 <div
-  class="flex max-w-lg place-items-center rounded-lg border bg-gray-100 p-2 dark:border-transparent dark:bg-gray-700"
-  use:clickOutside
-  on:outclick={() => dispatch('cancel')}
+  class="flex w-full place-items-center {suggestedPeople
+    ? 'rounded-t-lg border-b dark:border-immich-dark-gray'
+    : 'rounded-lg'}  bg-gray-100 p-2 dark:bg-gray-700"
 >
   <ImageThumbnail
     circle

+ 4 - 11
web/src/lib/components/faces-page/merge-suggestion-modal.svelte

@@ -17,16 +17,7 @@
 
   export let personMerge1: PersonResponseDto;
   export let personMerge2: PersonResponseDto;
-  export let people: PersonResponseDto[];
-  let potentialMergePeople: PersonResponseDto[] = people
-    .filter(
-      (person: PersonResponseDto) =>
-        personMerge2.name.toLowerCase() === person.name.toLowerCase() &&
-        person.id !== personMerge2.id &&
-        person.id !== personMerge1.id &&
-        !person.isHidden,
-    )
-    .slice(0, 3);
+  export let potentialMergePeople: PersonResponseDto[];
 
   let choosePersonToMerge = false;
 
@@ -48,7 +39,9 @@
         <h1 class="truncate px-4 py-4 font-medium text-immich-primary dark:text-immich-dark-primary">
           Merge faces - {title}
         </h1>
-        <CircleIconButton logo={Close} on:click={() => dispatch('close')} />
+        <div class="p-2">
+          <CircleIconButton logo={Close} on:click={() => dispatch('close')} />
+        </div>
       </div>
 
       <div class="flex items-center justify-center px-2 py-4 md:h-36 md:px-4 md:py-4">

+ 12 - 1
web/src/routes/(user)/people/+page.svelte

@@ -42,6 +42,7 @@
   let personName = '';
   let personMerge1: PersonResponseDto;
   let personMerge2: PersonResponseDto;
+  let potentialMergePeople: PersonResponseDto[] = [];
   let edittingPerson: PersonResponseDto | null = null;
 
   people.forEach((person: PersonResponseDto) => {
@@ -248,6 +249,7 @@
   };
 
   const submitNameChange = async () => {
+    potentialMergePeople = [];
     showChangeNameModal = false;
     if (!edittingPerson || personName === edittingPerson.name) {
       return;
@@ -264,6 +266,15 @@
     if (existingPerson) {
       personMerge2 = existingPerson;
       showMergeModal = true;
+      potentialMergePeople = people
+        .filter(
+          (person: PersonResponseDto) =>
+            personMerge2.name.toLowerCase() === person.name.toLowerCase() &&
+            person.id !== personMerge2.id &&
+            person.id !== personMerge1.id &&
+            !person.isHidden,
+        )
+        .slice(0, 3);
       return;
     }
     changeName();
@@ -332,7 +343,7 @@
     <MergeSuggestionModal
       {personMerge1}
       {personMerge2}
-      {people}
+      {potentialMergePeople}
       on:close={() => (showMergeModal = false)}
       on:reject={() => changeName()}
       on:confirm={(event) => handleMergeSameFace(event.detail)}

+ 97 - 32
web/src/routes/(user)/people/[personId]/+page.svelte

@@ -32,6 +32,7 @@
   import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
   import Plus from 'svelte-material-icons/Plus.svelte';
   import type { PageData } from './$types';
+  import { clickOutside } from '$lib/utils/click-outside';
 
   export let data: PageData;
 
@@ -58,12 +59,27 @@
   let people = data.people.people;
   let personMerge1: PersonResponseDto;
   let personMerge2: PersonResponseDto;
+  let potentialMergePeople: PersonResponseDto[] = [];
 
   let personName = '';
 
+  let name: string = data.person.name;
+  let suggestedPeople: PersonResponseDto[] = [];
+
   $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived);
   $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
 
+  $: {
+    suggestedPeople = !name
+      ? []
+      : people
+          .filter(
+            (person: PersonResponseDto) =>
+              person.name.toLowerCase().startsWith(name.toLowerCase()) && person.id !== data.person.id,
+          )
+          .slice(0, 5);
+  }
+
   onMount(() => {
     const action = $page.url.searchParams.get('action');
     if (action == 'merge') {
@@ -147,6 +163,14 @@
     }
   };
 
+  const handleSuggestPeople = (person: PersonResponseDto) => {
+    isEditingName = false;
+    potentialMergePeople = [];
+    personMerge1 = data.person;
+    personMerge2 = person;
+    viewMode = ViewMode.SUGGEST_MERGE;
+  };
+
   const changeName = async () => {
     viewMode = ViewMode.VIEW_ASSETS;
     data.person.name = personName;
@@ -183,6 +207,7 @@
   };
 
   const handleNameChange = async (name: string) => {
+    potentialMergePeople = [];
     personName = name;
 
     if (data.person.name === personName) {
@@ -196,6 +221,15 @@
     if (existingPerson) {
       personMerge2 = existingPerson;
       personMerge1 = data.person;
+      potentialMergePeople = people
+        .filter(
+          (person: PersonResponseDto) =>
+            personMerge2.name.toLowerCase() === person.name.toLowerCase() &&
+            person.id !== personMerge2.id &&
+            person.id !== personMerge1.id &&
+            !person.isHidden,
+        )
+        .slice(0, 3);
       viewMode = ViewMode.SUGGEST_MERGE;
       return;
     }
@@ -238,7 +272,7 @@
   <MergeSuggestionModal
     {personMerge1}
     {personMerge2}
-    {people}
+    {potentialMergePeople}
     on:close={() => (viewMode = ViewMode.VIEW_ASSETS)}
     on:reject={() => changeName()}
     on:confirm={(event) => handleMergeSameFace(event.detail)}
@@ -306,39 +340,70 @@
     >
       {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE}
         <!-- Face information block -->
-        <section class="flex place-items-center p-4 sm:px-6">
-          {#if isEditingName}
-            <EditNameInput
-              person={data.person}
-              on:change={(event) => handleNameChange(event.detail)}
-              on:cancel={() => handleCancelEditName()}
-            />
-          {:else}
-            <button on:click={() => (viewMode = ViewMode.VIEW_ASSETS)}>
-              <ImageThumbnail
-                circle
-                shadow
-                url={api.getPeopleThumbnailUrl(data.person.id)}
-                altText={data.person.name}
-                widthStyle="3.375rem"
-                heightStyle="3.375rem"
+        <div
+          role="button"
+          class="relative w-fit p-4 sm:px-6"
+          use:clickOutside
+          on:outclick={() => handleCancelEditName()}
+        >
+          <section class="flex w-96 place-items-center border-black">
+            {#if isEditingName}
+              <EditNameInput
+                person={data.person}
+                suggestedPeople={suggestedPeople.length > 0}
+                bind:name
+                on:change={(event) => handleNameChange(event.detail)}
               />
-            </button>
-
-            <button
-              title="Edit name"
-              class="px-4 text-immich-primary dark:text-immich-dark-primary"
-              on:click={() => (isEditingName = true)}
-            >
-              {#if data.person.name}
-                <p class="py-2 font-medium">{data.person.name}</p>
-              {:else}
-                <p class="w-fit font-medium">Add a name</p>
-                <p class="text-sm text-gray-500 dark:text-immich-gray">Find them fast by name with search</p>
-              {/if}
-            </button>
+            {:else}
+              <button on:click={() => (viewMode = ViewMode.VIEW_ASSETS)}>
+                <ImageThumbnail
+                  circle
+                  shadow
+                  url={api.getPeopleThumbnailUrl(data.person.id)}
+                  altText={data.person.name}
+                  widthStyle="3.375rem"
+                  heightStyle="3.375rem"
+                />
+              </button>
+
+              <button
+                title="Edit name"
+                class="px-4 text-immich-primary dark:text-immich-dark-primary"
+                on:click={() => (isEditingName = true)}
+              >
+                {#if data.person.name}
+                  <p class="py-2 font-medium">{data.person.name}</p>
+                {:else}
+                  <p class="w-fit font-medium">Add a name</p>
+                  <p class="text-sm text-gray-500 dark:text-immich-gray">Find them fast by name with search</p>
+                {/if}
+              </button>
+            {/if}
+          </section>
+          {#if isEditingName}
+            <div class="absolute z-[999] w-96">
+              {#each suggestedPeople as person, index (person.id)}
+                <div
+                  class="flex {index === suggestedPeople.length - 1
+                    ? 'rounded-b-lg'
+                    : 'border-b dark:border-immich-dark-gray'} place-items-center bg-gray-100 p-2 dark:bg-gray-700"
+                >
+                  <button class="flex w-full place-items-center" on:click={() => handleSuggestPeople(person)}>
+                    <ImageThumbnail
+                      circle
+                      shadow
+                      url={api.getPeopleThumbnailUrl(person.id)}
+                      altText={person.name}
+                      widthStyle="2rem"
+                      heightStyle="2rem"
+                    />
+                    <p class="ml-4 text-gray-700 dark:text-gray-100">{person.name}</p>
+                  </button>
+                </div>
+              {/each}
+            </div>
           {/if}
-        </section>
+        </div>
       {/if}
     </AssetGrid>
   {/key}