2023-12-05 16:43:15 +01:00
|
|
|
<script lang="ts">
|
|
|
|
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
2024-02-14 08:09:49 -05:00
|
|
|
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
2023-12-05 16:43:15 +01:00
|
|
|
import { boundingBoxesArray } from '$lib/stores/people.store';
|
2024-02-16 21:43:40 +01:00
|
|
|
import { websocketEvents } from '$lib/stores/websocket';
|
2024-02-27 08:37:37 -08:00
|
|
|
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
2024-02-14 08:09:49 -05:00
|
|
|
import { handleError } from '$lib/utils/handle-error';
|
2023-12-05 16:43:15 +01:00
|
|
|
import { getPersonNameWithHiddenValue } from '$lib/utils/person';
|
2024-02-14 06:38:57 -08:00
|
|
|
import {
|
|
|
|
|
AssetTypeEnum,
|
|
|
|
|
createPerson,
|
|
|
|
|
getAllPeople,
|
|
|
|
|
getFaces,
|
|
|
|
|
reassignFacesById,
|
|
|
|
|
type AssetFaceResponseDto,
|
|
|
|
|
type PersonResponseDto,
|
|
|
|
|
} from '@immich/sdk';
|
2024-02-14 08:09:49 -05:00
|
|
|
import { mdiArrowLeftThin, mdiRestart } from '@mdi/js';
|
|
|
|
|
import { createEventDispatcher, onMount } from 'svelte';
|
|
|
|
|
import { linear } from 'svelte/easing';
|
|
|
|
|
import { fly } from 'svelte/transition';
|
|
|
|
|
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
|
|
|
|
import Icon from '../elements/icon.svelte';
|
|
|
|
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
|
|
|
|
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
|
2023-12-05 16:43:15 +01:00
|
|
|
|
|
|
|
|
export let assetId: string;
|
2023-12-08 05:18:33 +01:00
|
|
|
export let assetType: AssetTypeEnum;
|
2023-12-05 16:43:15 +01:00
|
|
|
|
|
|
|
|
// keep track of the changes
|
2024-04-19 04:55:11 +02:00
|
|
|
let peopleToCreate: string[] = [];
|
|
|
|
|
let assetFaceGenerated: string[] = [];
|
2023-12-05 16:43:15 +01:00
|
|
|
|
|
|
|
|
// faces
|
|
|
|
|
let peopleWithFaces: AssetFaceResponseDto[] = [];
|
2024-04-19 04:55:11 +02:00
|
|
|
let selectedPersonToReassign: Record<string, PersonResponseDto> = {};
|
|
|
|
|
let selectedPersonToCreate: Record<string, string> = {};
|
|
|
|
|
let editedPerson: PersonResponseDto;
|
2023-12-05 16:43:15 +01:00
|
|
|
|
|
|
|
|
// loading spinners
|
|
|
|
|
let isShowLoadingDone = false;
|
|
|
|
|
let isShowLoadingPeople = false;
|
|
|
|
|
|
|
|
|
|
// search people
|
|
|
|
|
let showSeletecFaces = false;
|
|
|
|
|
let allPeople: PersonResponseDto[] = [];
|
|
|
|
|
|
|
|
|
|
// timers
|
2024-02-27 11:01:11 -08:00
|
|
|
let loaderLoadingDoneTimeout: ReturnType<typeof setTimeout>;
|
|
|
|
|
let automaticRefreshTimeout: ReturnType<typeof setTimeout>;
|
2023-12-05 16:43:15 +01:00
|
|
|
|
2024-04-19 04:55:11 +02:00
|
|
|
const thumbnailWidth = '90px';
|
|
|
|
|
|
2023-12-15 03:54:21 +01:00
|
|
|
const dispatch = createEventDispatcher<{
|
|
|
|
|
close: void;
|
|
|
|
|
refresh: void;
|
|
|
|
|
}>();
|
2023-12-05 16:43:15 +01:00
|
|
|
|
2024-02-16 21:43:40 +01:00
|
|
|
async function loadPeople() {
|
2024-01-28 01:54:31 +01:00
|
|
|
const timeout = setTimeout(() => (isShowLoadingPeople = true), timeBeforeShowLoadingSpinner);
|
2023-12-05 16:43:15 +01:00
|
|
|
try {
|
2024-02-14 08:09:49 -05:00
|
|
|
const { people } = await getAllPeople({ withHidden: true });
|
|
|
|
|
allPeople = people;
|
2024-02-13 17:07:37 -05:00
|
|
|
peopleWithFaces = await getFaces({ id: assetId });
|
2023-12-05 16:43:15 +01:00
|
|
|
} catch (error) {
|
|
|
|
|
handleError(error, "Can't get faces");
|
|
|
|
|
} finally {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
}
|
|
|
|
|
isShowLoadingPeople = false;
|
2024-02-16 21:43:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onPersonThumbnail = (personId: string) => {
|
2024-04-19 04:55:11 +02:00
|
|
|
assetFaceGenerated.push(personId);
|
2024-02-16 21:43:40 +01:00
|
|
|
if (
|
2024-04-19 04:55:11 +02:00
|
|
|
isEqual(assetFaceGenerated, peopleToCreate) &&
|
2024-02-16 21:43:40 +01:00
|
|
|
loaderLoadingDoneTimeout &&
|
|
|
|
|
automaticRefreshTimeout &&
|
2024-04-19 04:55:11 +02:00
|
|
|
Object.keys(selectedPersonToCreate).length === peopleToCreate.length
|
2024-02-16 21:43:40 +01:00
|
|
|
) {
|
|
|
|
|
clearTimeout(loaderLoadingDoneTimeout);
|
|
|
|
|
clearTimeout(automaticRefreshTimeout);
|
|
|
|
|
dispatch('refresh');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
2024-02-27 08:37:37 -08:00
|
|
|
handlePromiseError(loadPeople());
|
2024-02-16 21:43:40 +01:00
|
|
|
return websocketEvents.on('on_person_thumbnail', onPersonThumbnail);
|
2023-12-05 16:43:15 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const isEqual = (a: string[], b: string[]): boolean => {
|
|
|
|
|
return b.every((valueB) => a.includes(valueB));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBackButton = () => {
|
|
|
|
|
dispatch('close');
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-19 04:55:11 +02:00
|
|
|
const handleReset = (id: string) => {
|
|
|
|
|
if (selectedPersonToReassign[id]) {
|
|
|
|
|
delete selectedPersonToReassign[id];
|
|
|
|
|
|
|
|
|
|
// trigger reactivity
|
|
|
|
|
selectedPersonToReassign = selectedPersonToReassign;
|
2023-12-05 16:43:15 +01:00
|
|
|
}
|
2024-04-19 04:55:11 +02:00
|
|
|
if (selectedPersonToCreate[id]) {
|
|
|
|
|
delete selectedPersonToCreate[id];
|
|
|
|
|
|
|
|
|
|
// trigger reactivity
|
|
|
|
|
selectedPersonToCreate = selectedPersonToCreate;
|
2023-12-05 16:43:15 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleEditFaces = async () => {
|
2024-01-28 01:54:31 +01:00
|
|
|
loaderLoadingDoneTimeout = setTimeout(() => (isShowLoadingDone = true), timeBeforeShowLoadingSpinner);
|
2024-04-19 04:55:11 +02:00
|
|
|
const numberOfChanges = Object.keys(selectedPersonToCreate).length + Object.keys(selectedPersonToReassign).length;
|
|
|
|
|
|
2023-12-05 16:43:15 +01:00
|
|
|
if (numberOfChanges > 0) {
|
|
|
|
|
try {
|
2024-04-19 04:55:11 +02:00
|
|
|
for (const personWithFace of peopleWithFaces) {
|
|
|
|
|
const personId = selectedPersonToReassign[personWithFace.id]?.id;
|
2023-12-05 16:43:15 +01:00
|
|
|
|
|
|
|
|
if (personId) {
|
2024-02-13 17:07:37 -05:00
|
|
|
await reassignFacesById({
|
2023-12-05 16:43:15 +01:00
|
|
|
id: personId,
|
2024-04-19 04:55:11 +02:00
|
|
|
faceDto: { id: personWithFace.id },
|
2023-12-05 16:43:15 +01:00
|
|
|
});
|
2024-04-19 04:55:11 +02:00
|
|
|
} else if (selectedPersonToCreate[personWithFace.id]) {
|
2024-03-07 15:34:57 -05:00
|
|
|
const data = await createPerson({ personCreateDto: {} });
|
2024-04-19 04:55:11 +02:00
|
|
|
peopleToCreate.push(data.id);
|
2024-02-13 17:07:37 -05:00
|
|
|
await reassignFacesById({
|
2023-12-05 16:43:15 +01:00
|
|
|
id: data.id,
|
2024-04-19 04:55:11 +02:00
|
|
|
faceDto: { id: personWithFace.id },
|
2023-12-05 16:43:15 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
notificationController.show({
|
|
|
|
|
message: `Edited ${numberOfChanges} ${numberOfChanges > 1 ? 'people' : 'person'}`,
|
|
|
|
|
type: NotificationType.Info,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
handleError(error, "Can't apply changes");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isShowLoadingDone = false;
|
2024-04-19 04:55:11 +02:00
|
|
|
if (peopleToCreate.length === 0) {
|
2023-12-05 16:43:15 +01:00
|
|
|
clearTimeout(loaderLoadingDoneTimeout);
|
|
|
|
|
dispatch('refresh');
|
|
|
|
|
} else {
|
2024-02-02 04:18:00 +01:00
|
|
|
automaticRefreshTimeout = setTimeout(() => dispatch('refresh'), 15_000);
|
2023-12-05 16:43:15 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCreatePerson = (newFeaturePhoto: string | null) => {
|
2024-04-19 04:55:11 +02:00
|
|
|
const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id);
|
2023-12-05 16:43:15 +01:00
|
|
|
if (newFeaturePhoto && personToUpdate) {
|
2024-04-19 04:55:11 +02:00
|
|
|
selectedPersonToCreate[personToUpdate.id] = newFeaturePhoto;
|
2023-12-05 16:43:15 +01:00
|
|
|
}
|
|
|
|
|
showSeletecFaces = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleReassignFace = (person: PersonResponseDto | null) => {
|
2024-04-19 04:55:11 +02:00
|
|
|
const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id);
|
|
|
|
|
if (person && personToUpdate) {
|
|
|
|
|
selectedPersonToReassign[personToUpdate.id] = person;
|
2023-12-05 16:43:15 +01:00
|
|
|
showSeletecFaces = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-19 04:55:11 +02:00
|
|
|
const handlePersonPicker = (person: PersonResponseDto | null) => {
|
|
|
|
|
if (person) {
|
|
|
|
|
editedPerson = person;
|
|
|
|
|
showSeletecFaces = true;
|
|
|
|
|
}
|
2023-12-05 16:43:15 +01:00
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<section
|
|
|
|
|
transition:fly={{ x: 360, duration: 100, easing: linear }}
|
2023-12-06 00:05:22 +01:00
|
|
|
class="absolute top-0 z-[2000] h-full w-[360px] overflow-x-hidden p-2 bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg"
|
2023-12-05 16:43:15 +01:00
|
|
|
>
|
|
|
|
|
<div class="flex place-items-center justify-between gap-2">
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
class="flex place-content-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
|
|
|
|
|
on:click={handleBackButton}
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<Icon path={mdiArrowLeftThin} size="24" />
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">Edit faces</p>
|
|
|
|
|
</div>
|
|
|
|
|
{#if !isShowLoadingDone}
|
|
|
|
|
<button
|
|
|
|
|
class="justify-self-end rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
|
|
|
|
|
on:click={() => handleEditFaces()}
|
|
|
|
|
>
|
|
|
|
|
Done
|
|
|
|
|
</button>
|
|
|
|
|
{:else}
|
|
|
|
|
<LoadingSpinner />
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="px-4 py-4 text-sm">
|
|
|
|
|
<div class="mt-4 flex flex-wrap gap-2">
|
|
|
|
|
{#if isShowLoadingPeople}
|
|
|
|
|
<div class="flex w-full justify-center">
|
|
|
|
|
<LoadingSpinner />
|
|
|
|
|
</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">
|
2024-04-19 04:55:11 +02:00
|
|
|
{#if selectedPersonToCreate[face.id]}
|
|
|
|
|
<ImageThumbnail
|
|
|
|
|
curve
|
|
|
|
|
shadow
|
|
|
|
|
url={selectedPersonToCreate[face.id]}
|
|
|
|
|
altText={selectedPersonToCreate[face.id]}
|
|
|
|
|
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 || selectedPersonToReassign[face.id].id}
|
|
|
|
|
title={getPersonNameWithHiddenValue(
|
|
|
|
|
selectedPersonToReassign[face.id].name,
|
|
|
|
|
face.person?.isHidden,
|
|
|
|
|
)}
|
|
|
|
|
widthStyle={thumbnailWidth}
|
|
|
|
|
heightStyle={thumbnailWidth}
|
|
|
|
|
hidden={selectedPersonToReassign[face.id].isHidden}
|
|
|
|
|
/>
|
|
|
|
|
{: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}
|
2023-12-05 16:43:15 +01:00
|
|
|
</div>
|
2024-04-19 04:55:11 +02:00
|
|
|
|
|
|
|
|
{#if !selectedPersonToCreate[face.id]}
|
2023-12-05 16:43:15 +01:00
|
|
|
<p class="relative mt-1 truncate font-medium" title={face.person?.name}>
|
2024-04-19 04:55:11 +02:00
|
|
|
{#if selectedPersonToReassign[face.id]?.id}
|
|
|
|
|
{selectedPersonToReassign[face.id]?.name}
|
2023-12-05 16:43:15 +01:00
|
|
|
{:else}
|
|
|
|
|
{face.person?.name}
|
|
|
|
|
{/if}
|
|
|
|
|
</p>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<div class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full bg-blue-700">
|
2024-04-19 04:55:11 +02:00
|
|
|
{#if selectedPersonToCreate[face.id] || selectedPersonToReassign[face.id]}
|
|
|
|
|
<button on:click={() => handleReset(face.id)} class="flex h-full w-full">
|
2023-12-05 16:43:15 +01:00
|
|
|
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
|
|
|
|
|
<div>
|
|
|
|
|
<Icon path={mdiRestart} size={18} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
{:else}
|
2024-04-19 04:55:11 +02:00
|
|
|
<button on:click={() => handlePersonPicker(face.person)} class="flex h-full w-full">
|
2023-12-05 16:43:15 +01:00
|
|
|
<div
|
|
|
|
|
class="absolute left-1/2 top-1/2 h-[2px] w-[14px] translate-x-[-50%] translate-y-[-50%] transform bg-white"
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
{/each}
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{#if showSeletecFaces}
|
|
|
|
|
<AssignFaceSidePanel
|
|
|
|
|
{peopleWithFaces}
|
|
|
|
|
{allPeople}
|
2024-04-19 04:55:11 +02:00
|
|
|
{editedPerson}
|
2023-12-08 05:18:33 +01:00
|
|
|
{assetType}
|
|
|
|
|
{assetId}
|
2023-12-05 16:43:15 +01:00
|
|
|
on:close={() => (showSeletecFaces = false)}
|
|
|
|
|
on:createPerson={(event) => handleCreatePerson(event.detail)}
|
|
|
|
|
on:reassign={(event) => handleReassignFace(event.detail)}
|
|
|
|
|
/>
|
|
|
|
|
{/if}
|