diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index 3ac09de78a..96b37637fb 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -14,6 +14,7 @@ export let shadow = false; export let circle = false; export let hidden = false; + export let border = false; let complete = false; export let eyeColor = 'white'; @@ -26,7 +27,9 @@ style:opacity={hidden ? '0.5' : '1'} src={url} alt={altText} - class="object-cover transition duration-300" + class="object-cover transition duration-300 {border + ? 'border-[3px] border-immich-dark-primary/80 hover:border-immich-primary' + : ''}" class:rounded-lg={curve} class:shadow-lg={shadow} class:rounded-full={circle} diff --git a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte new file mode 100644 index 0000000000..56beed9f5c --- /dev/null +++ b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte @@ -0,0 +1,128 @@ + + +
+
+
+

+ Merge faces - {title} +

+ dispatch('close')} /> +
+ +
+ {#if !choosePersonToMerge} +
+ +
+
+ ([personMerge1, personMerge2] = [personMerge2, personMerge1])} + /> +
+ + + {:else} +
+
+ +
+
+
+ {#each potentialMergePeople as person (person.id)} +
+ +
+ {/each} +
+
+
+ {/if} +
+ +
+

Are these the same face?

+
+
+

They will be merged together

+
+
+ + +
+
+
diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 274205451c..29c192a878 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -19,6 +19,7 @@ import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; import { onDestroy, onMount } from 'svelte'; import { browser } from '$app/environment'; + import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte'; export let data: PageData; let selectHidden = false; @@ -33,6 +34,13 @@ let showLoadingSpinner = false; let toggleVisibility = false; + let showChangeNameModal = false; + let showMergeModal = false; + let personName = ''; + let personMerge1: PersonResponseDto; + let personMerge2: PersonResponseDto; + let edittingPerson: PersonResponseDto | null = null; + people.forEach((person: PersonResponseDto) => { initialHiddenValues[person.id] = person.isHidden; }); @@ -136,13 +144,60 @@ toggleVisibility = false; }; - let showChangeNameModal = false; - let personName = ''; - let edittingPerson: PersonResponseDto | null = null; + const handleMergeSameFace = async (response: [PersonResponseDto, PersonResponseDto]) => { + const [personToMerge, personToBeMergedIn] = response; + showMergeModal = false; + + if (!edittingPerson) { + return; + } + try { + await api.personApi.mergePerson({ + id: personMerge2.id, + mergePersonDto: { ids: [personToMerge.id] }, + }); + countVisiblePeople--; + people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id); + + notificationController.show({ + message: 'Merge faces succesfully', + type: NotificationType.Info, + }); + } catch (error) { + handleError(error, 'Unable to save name'); + } + if (personToBeMergedIn.name !== personName && edittingPerson.id === personToBeMergedIn.id) { + /* + * + * If the user merges one of the suggested people into the person he's editing it, it's merging the suggested person AND renames + * the person he's editing + * + */ + try { + await api.personApi.updatePerson({ id: personToBeMergedIn.id, personUpdateDto: { name: personName } }); + for (const person of people) { + if (person.id === personToBeMergedIn.id) { + person.name = personName; + break; + } + } + notificationController.show({ + message: 'Change name succesfully', + type: NotificationType.Info, + }); + + // trigger reactivity + people = people; + } catch (error) { + handleError(error, 'Unable to save name'); + } + } + }; const handleChangeName = ({ detail }: CustomEvent) => { showChangeNameModal = true; personName = detail.name; + personMerge1 = detail; edittingPerson = detail; }; @@ -182,33 +237,73 @@ }; const submitNameChange = async () => { + showChangeNameModal = false; + if (!edittingPerson) { + return; + } + if (personName === edittingPerson.name) { + return; + } + // We check if another person has the same name as the name entered by the user + + const existingPerson = people.find( + (person: PersonResponseDto) => + person.name.toLowerCase() === personName.toLowerCase() && + edittingPerson && + person.id !== edittingPerson.id && + person.name, + ); + if (existingPerson) { + personMerge2 = existingPerson; + showMergeModal = true; + return; + } + changeName(); + }; + + const changeName = async () => { + showMergeModal = false; + showChangeNameModal = false; + + if (!edittingPerson) { + return; + } try { - if (edittingPerson) { - const { data: updatedPerson } = await api.personApi.updatePerson({ - id: edittingPerson.id, - personUpdateDto: { name: personName }, - }); + const { data: updatedPerson } = await api.personApi.updatePerson({ + id: edittingPerson.id, + personUpdateDto: { name: personName }, + }); - people = people.map((person: PersonResponseDto) => { - if (person.id === updatedPerson.id) { - return updatedPerson; - } - return person; - }); + people = people.map((person: PersonResponseDto) => { + if (person.id === updatedPerson.id) { + return updatedPerson; + } + return person; + }); - showChangeNameModal = false; - - notificationController.show({ - message: 'Change name succesfully', - type: NotificationType.Info, - }); - } + notificationController.show({ + message: 'Change name succesfully', + type: NotificationType.Info, + }); } catch (error) { handleError(error, 'Unable to save name'); } }; +{#if showMergeModal} + (showMergeModal = false)}> + (showMergeModal = false)} + on:reject={() => changeName()} + on:confirm={(event) => handleMergeSameFace(event.detail)} + /> + +{/if} + {#if countTotalPeople > 0} diff --git a/web/src/routes/(user)/people/[personId]/+page.server.ts b/web/src/routes/(user)/people/[personId]/+page.server.ts index 564f4a0fda..bfe80031e7 100644 --- a/web/src/routes/(user)/people/[personId]/+page.server.ts +++ b/web/src/routes/(user)/people/[personId]/+page.server.ts @@ -10,11 +10,13 @@ export const load = (async ({ locals, parent, params }) => { const { data: person } = await locals.api.personApi.getPerson({ id: params.personId }); const { data: assets } = await locals.api.personApi.getPersonAssets({ id: params.personId }); + const { data: people } = await locals.api.personApi.getAllPeople({ withHidden: false }); return { user, assets, person, + people, meta: { title: person.name || 'Person', }, diff --git a/web/src/routes/(user)/people/[personId]/+page.svelte b/web/src/routes/(user)/people/[personId]/+page.svelte index 76713d73ab..0ccd27e35b 100644 --- a/web/src/routes/(user)/people/[personId]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/+page.svelte @@ -1,5 +1,5 @@ +{#if showMergeModal} + (showMergeModal = false)}> + (showMergeModal = false)} + on:reject={() => changeName()} + on:confirm={(event) => handleMergeSameFace(event.detail)} + /> + +{/if} + {#if isMultiSelectionMode} (selectedAssets = new Set())}>