feat(web): Option to assign people to unassigned faces (#9773)

* added unassigned faces to people edit

* svelte fix

* fix format

* Captialized unassigned person name, removed person id from alttext, fixed problem with multiple faces per person

* Added faces to the getAssetInfo API endpoint

* Updated openApi clients

* Readded the photoeditor dependency

* fixed lint/format

* fixed photoViewer type

* changes getAssetInfo.faces to only include unassigned faces

* fix: bad merge

* title

* logic

---------

Co-authored-by: Jan108 <dasJan108@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jan108 2024-06-05 09:26:00 +02:00 committed by GitHub
parent 588860455f
commit b2761b12d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 219 additions and 151 deletions

View file

@ -7,14 +7,16 @@
import { handleError } from '$lib/utils/handle-error';
import { getPersonNameWithHiddenValue } from '$lib/utils/person';
import {
AssetTypeEnum,
createPerson,
getAllPeople,
getFaces,
reassignFacesById,
AssetTypeEnum,
type AssetFaceResponseDto,
type PersonResponseDto,
} from '@immich/sdk';
import { mdiAccountOff } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte';
import { mdiArrowLeftThin, mdiMinus, mdiRestart } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import { linear } from 'svelte/easing';
@ -23,6 +25,8 @@
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { zoomImageToBase64 } from '$lib/utils/people-utils';
import { photoViewer } from '$lib/stores/assets.store';
import { t } from 'svelte-i18n';
export let assetId: string;
@ -36,7 +40,6 @@
let peopleWithFaces: AssetFaceResponseDto[] = [];
let selectedPersonToReassign: Record<string, PersonResponseDto> = {};
let selectedPersonToCreate: Record<string, string> = {};
let editedPerson: PersonResponseDto;
let editedFace: AssetFaceResponseDto;
// loading spinners
@ -171,11 +174,8 @@
};
const handleFacePicker = (face: AssetFaceResponseDto) => {
if (face.person) {
editedFace = face;
editedPerson = face.person;
showSelectedFaces = true;
}
editedFace = face;
showSelectedFaces = true;
};
</script>
@ -209,91 +209,125 @@
</div>
{:else}
{#each peopleWithFaces as face, index}
{#if face.person}
<div class="relative z-[20001] h-[115px] w-[95px]">
<div
role="button"
tabindex={index}
class="absolute left-0 top-0 h-[90px] w-[90px] cursor-default"
on:focus={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
on:mouseover={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
on:mouseleave={() => ($boundingBoxesArray = [])}
>
<div class="relative">
{#if selectedPersonToCreate[face.id]}
{@const personName = face.person ? face.person?.name : 'Unassigned'}
<div class="relative z-[20001] h-[115px] w-[95px]">
<div
role="button"
tabindex={index}
class="absolute left-0 top-0 h-[90px] w-[90px] cursor-default"
on:focus={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
on:mouseover={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
on:mouseleave={() => ($boundingBoxesArray = [])}
>
<div class="relative">
{#if selectedPersonToCreate[face.id]}
<ImageThumbnail
curve
shadow
url={selectedPersonToCreate[face.id]}
altText={'New person'}
title={'New person'}
widthStyle={thumbnailWidth}
heightStyle={thumbnailWidth}
/>
{:else if selectedPersonToReassign[face.id]}
<ImageThumbnail
curve
shadow
url={getPeopleThumbnailUrl(selectedPersonToReassign[face.id].id)}
altText={selectedPersonToReassign[face.id].name}
title={getPersonNameWithHiddenValue(
selectedPersonToReassign[face.id].name,
selectedPersonToReassign[face.id]?.isHidden,
)}
widthStyle={thumbnailWidth}
heightStyle={thumbnailWidth}
hidden={selectedPersonToReassign[face.id].isHidden}
/>
{:else if face.person}
<ImageThumbnail
curve
shadow
url={getPeopleThumbnailUrl(face.person.id)}
altText={face.person.name}
title={getPersonNameWithHiddenValue(face.person.name, face.person.isHidden)}
widthStyle={thumbnailWidth}
heightStyle={thumbnailWidth}
hidden={face.person.isHidden}
/>
{:else}
{#await zoomImageToBase64(face, assetId, assetType, $photoViewer)}
<ImageThumbnail
curve
shadow
url={selectedPersonToCreate[face.id]}
altText={selectedPersonToCreate[face.id]}
title={$t('new_person')}
widthStyle={thumbnailWidth}
heightStyle={thumbnailWidth}
url="/src/lib/assets/no-thumbnail.png"
altText="Unassigned"
title="Unassigned"
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
hidden={false}
/>
{:else if selectedPersonToReassign[face.id]}
{:then data}
<ImageThumbnail
curve
shadow
url={getPeopleThumbnailUrl(selectedPersonToReassign[face.id].id)}
altText={selectedPersonToReassign[face.id]?.name || selectedPersonToReassign[face.id].id}
title={getPersonNameWithHiddenValue(
selectedPersonToReassign[face.id].name,
face.person?.isHidden,
)}
widthStyle={thumbnailWidth}
heightStyle={thumbnailWidth}
hidden={selectedPersonToReassign[face.id].isHidden}
url={data === null ? '/src/lib/assets/no-thumbnail.png' : data}
altText="Unassigned"
title="Unassigned"
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
hidden={false}
/>
{:else}
<ImageThumbnail
curve
shadow
url={getPeopleThumbnailUrl(face.person.id)}
altText={face.person.name || face.person.id}
title={getPersonNameWithHiddenValue(face.person.name, face.person.isHidden)}
widthStyle={thumbnailWidth}
heightStyle={thumbnailWidth}
hidden={face.person.isHidden}
/>
{/if}
</div>
{#if !selectedPersonToCreate[face.id]}
<p class="relative mt-1 truncate font-medium" title={face.person?.name}>
{#if selectedPersonToReassign[face.id]?.id}
{selectedPersonToReassign[face.id]?.name}
{:else}
{face.person?.name}
{/if}
</p>
{/await}
{/if}
</div>
<div class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full">
{#if selectedPersonToCreate[face.id] || selectedPersonToReassign[face.id]}
<CircleIconButton
color="primary"
icon={mdiRestart}
title={$t('reset')}
size="18"
padding="1"
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
on:click={() => handleReset(face.id)}
/>
{#if !selectedPersonToCreate[face.id]}
<p class="relative mt-1 truncate font-medium" title={personName}>
{#if selectedPersonToReassign[face.id]?.id}
{selectedPersonToReassign[face.id]?.name}
{:else}
<CircleIconButton
color="primary"
icon={mdiMinus}
title={$t('select_new_face')}
size="18"
padding="1"
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
on:click={() => handleFacePicker(face)}
/>
<span class={personName == 'Unassigned' ? 'dark:text-gray-500' : ''}>{personName}</span>
{/if}
</div>
</p>
{/if}
<div class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full">
{#if selectedPersonToCreate[face.id] || selectedPersonToReassign[face.id]}
<CircleIconButton
color="primary"
icon={mdiRestart}
title="Reset"
size="18"
padding="1"
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
on:click={() => handleReset(face.id)}
/>
{:else}
<CircleIconButton
color="primary"
icon={mdiMinus}
title="Select new face"
size="18"
padding="1"
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
on:click={() => handleFacePicker(face)}
/>
{/if}
</div>
<div class="absolute right-[25px] -top-[5px] h-[20px] w-[20px] rounded-full">
{#if !selectedPersonToCreate[face.id] && !selectedPersonToReassign[face.id] && !face.person}
<div
class="flex place-content-center place-items-center rounded-full bg-[#d3d3d3] p-1 transition-all absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
>
<Icon color="primary" path={mdiAccountOff} ariaLabel="Just a face" size="18" />
</div>
{/if}
</div>
</div>
{/if}
</div>
{/each}
{/if}
</div>
@ -302,11 +336,10 @@
{#if showSelectedFaces}
<AssignFaceSidePanel
{peopleWithFaces}
{allPeople}
{editedPerson}
{assetType}
{editedFace}
{assetId}
{assetType}
on:close={() => (showSelectedFaces = false)}
on:createPerson={(event) => handleCreatePerson(event.detail)}
on:reassign={(event) => handleReassignFace(event.detail)}