mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-12-08 20:29:05 +00:00
feat(web): suggest to merge people faces when renaming a person name (#3399)
* feat: propose to merge faced based on the name * responsive * drop down menu * add border * improvements * improvements * improvements * add comments * responsive * responsive * feat: use FullScreenModal * responsive * pr feeback * pr feeback * pr feeback * responsive * pr feeback * pr feeback * styling * fix test --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -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<PersonResponseDto>) => {
|
||||
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');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if showMergeModal}
|
||||
<FullScreenModal on:clickOutside={() => (showMergeModal = false)}>
|
||||
<MergeSuggestionModal
|
||||
{personMerge1}
|
||||
{personMerge2}
|
||||
{people}
|
||||
on:close={() => (showMergeModal = false)}
|
||||
on:reject={() => changeName()}
|
||||
on:confirm={(event) => handleMergeSameFace(event.detail)}
|
||||
/>
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
||||
<UserPageLayout user={data.user} title="People">
|
||||
<svelte:fragment slot="buttons">
|
||||
{#if countTotalPeople > 0}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { afterNavigate, goto, invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
|
||||
@@ -15,7 +15,7 @@
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AssetResponseDto, api } from '@api';
|
||||
import { AssetResponseDto, PersonResponseDto, api } from '@api';
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
@@ -30,6 +30,8 @@
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
let isEditingName = false;
|
||||
@@ -37,6 +39,13 @@
|
||||
let showMergeFacePanel = false;
|
||||
let previousRoute: string = AppRoute.EXPLORE;
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
let showMergeModal = false;
|
||||
let people = data.people.people;
|
||||
let personMerge1: PersonResponseDto;
|
||||
let personMerge2: PersonResponseDto;
|
||||
|
||||
let personName = '';
|
||||
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
$: isAllArchive = Array.from(selectedAssets).every((asset) => asset.isArchived);
|
||||
$: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite);
|
||||
@@ -56,16 +65,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
const handleNameChange = async (name: string) => {
|
||||
try {
|
||||
isEditingName = false;
|
||||
data.person.name = name;
|
||||
await api.personApi.updatePerson({ id: data.person.id, personUpdateDto: { name } });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save name');
|
||||
}
|
||||
};
|
||||
|
||||
const onAssetDelete = (assetId: string) => {
|
||||
data.assets = data.assets.filter((asset: AssetResponseDto) => asset.id !== assetId);
|
||||
};
|
||||
@@ -91,8 +90,92 @@
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMergeSameFace = async (response: [PersonResponseDto, PersonResponseDto]) => {
|
||||
const [personToMerge, personToBeMergedIn] = response;
|
||||
showMergeModal = false;
|
||||
try {
|
||||
await api.personApi.mergePerson({
|
||||
id: personToBeMergedIn.id,
|
||||
mergePersonDto: { ids: [personToMerge.id] },
|
||||
});
|
||||
notificationController.show({
|
||||
message: 'Merge faces succesfully',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id);
|
||||
if (personToBeMergedIn.name != personName && data.person.id === personToBeMergedIn.id) {
|
||||
changeName();
|
||||
invalidateAll();
|
||||
return;
|
||||
}
|
||||
goto(`${AppRoute.PEOPLE}/${personToBeMergedIn.id}`, { replaceState: true });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save name');
|
||||
}
|
||||
};
|
||||
|
||||
const changeName = async () => {
|
||||
showMergeModal = false;
|
||||
data.person.name = personName;
|
||||
try {
|
||||
isEditingName = false;
|
||||
|
||||
const { data: updatedPerson } = await api.personApi.updatePerson({
|
||||
id: data.person.id,
|
||||
personUpdateDto: { name: personName },
|
||||
});
|
||||
|
||||
people = people.map((person: PersonResponseDto) => {
|
||||
if (person.id === updatedPerson.id) {
|
||||
return updatedPerson;
|
||||
}
|
||||
return person;
|
||||
});
|
||||
|
||||
notificationController.show({
|
||||
message: 'Change name succesfully',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save name');
|
||||
}
|
||||
};
|
||||
|
||||
const handleNameChange = async (name: string) => {
|
||||
personName = name;
|
||||
|
||||
if (data.person.name === personName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingPerson = people.find(
|
||||
(person: PersonResponseDto) =>
|
||||
person.name.toLowerCase() === personName.toLowerCase() && person.id !== data.person.id && person.name,
|
||||
);
|
||||
if (existingPerson) {
|
||||
personMerge2 = existingPerson;
|
||||
personMerge1 = data.person;
|
||||
showMergeModal = true;
|
||||
return;
|
||||
}
|
||||
changeName();
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if showMergeModal}
|
||||
<FullScreenModal on:clickOutside={() => (showMergeModal = false)}>
|
||||
<MergeSuggestionModal
|
||||
{personMerge1}
|
||||
{personMerge2}
|
||||
{people}
|
||||
on:close={() => (showMergeModal = false)}
|
||||
on:reject={() => changeName()}
|
||||
on:confirm={(event) => handleMergeSameFace(event.detail)}
|
||||
/>
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
||||
{#if isMultiSelectionMode}
|
||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
||||
<CreateSharedLink />
|
||||
|
||||
Reference in New Issue
Block a user