mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
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:
parent
588860455f
commit
b2761b12d1
8 changed files with 219 additions and 151 deletions
|
|
@ -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)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue