Browse Source

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
Alex 1 năm trước cách đây
mục cha
commit
02b70e693c

+ 64 - 0
web/src/lib/components/faces-page/people-card.svelte

@@ -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}

+ 100 - 34
web/src/routes/(user)/people/+page.svelte

@@ -1,46 +1,112 @@
 <script lang="ts">
-  import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
-  import { api } from '@api';
   import AccountOff from 'svelte-material-icons/AccountOff.svelte';
   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;
+
+  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>
 
 <UserPageLayout user={data.user} showUploadButton title="People">
-  {#if data.people.length > 0}
-    <div class="pl-4">
-      <div class="flex flex-row flex-wrap gap-1">
-        {#each data.people as person (person.id)}
-          <div 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>
-          </div>
-        {/each}
+  <section>
+    {#if data.people.length > 0}
+      <div class="pl-4">
+        <div class="flex flex-row flex-wrap gap-1">
+          {#each data.people as person (person.id)}
+            <PeopleCard {person} on:change-name={handleChangeName} on:merge-faces={handleMergeFaces} />
+          {/each}
+        </div>
+      </div>
+    {: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>
-    </div>
-  {: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>
+    {/if}
+  </section>
+
+  {#if showChangeNameModal}
+    <FullScreenModal on:clickOutside={() => (showChangeNameModal = false)}>
+      <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 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>
+    </FullScreenModal>
   {/if}
 </UserPageLayout>

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

@@ -29,6 +29,7 @@
     notificationController,
   } from '$lib/components/shared-components/notification/notification';
   import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte';
+  import { onMount } from 'svelte';
 
   export let data: PageData;
   let isEditingName = false;
@@ -42,6 +43,12 @@
 
   $: showAssets = !showMergeFacePanel && !showFaceThumbnailSelection;
 
+  onMount(() => {
+    const action = $page.url.searchParams.get('action');
+    if (action == 'merge') {
+      showMergeFacePanel = true;
+    }
+  });
   afterNavigate(({ from }) => {
     // Prevent setting previousRoute to the current page.
     if (from && from.route.id !== $page.route.id) {