mirror of
https://github.com/immich-app/immich.git
synced 2025-03-18 02:31:28 -05:00
feat(web): better person naming interface (#16631)
* feat(web): better person naming interface * feat(web): better person naming interface * feat(web): better person naming interface * feat(web): better person naming interface * feat(web): better person naming interface * feat(web): better person naming interface * feat(web): better person naming interface
This commit is contained in:
parent
6caa11d079
commit
c80afea468
3 changed files with 104 additions and 152 deletions
web/src
lib/components/faces-page
routes/(user)/people
|
@ -6,7 +6,6 @@
|
|||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { type PersonResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
mdiAccountEditOutline,
|
||||
mdiAccountMultipleCheckOutline,
|
||||
mdiCalendarEditOutline,
|
||||
mdiDotsVertical,
|
||||
|
@ -22,22 +21,13 @@
|
|||
interface Props {
|
||||
person: PersonResponseDto;
|
||||
preload?: boolean;
|
||||
onChangeName: () => void;
|
||||
onSetBirthDate: () => void;
|
||||
onMergePeople: () => void;
|
||||
onHidePerson: () => void;
|
||||
onToggleFavorite: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
person,
|
||||
preload = false,
|
||||
onChangeName,
|
||||
onSetBirthDate,
|
||||
onMergePeople,
|
||||
onHidePerson,
|
||||
onToggleFavorite,
|
||||
}: Props = $props();
|
||||
let { person, preload = false, onSetBirthDate, onMergePeople, onHidePerson, onToggleFavorite }: Props = $props();
|
||||
|
||||
let showVerticalDots = $state(false);
|
||||
</script>
|
||||
|
@ -63,6 +53,7 @@
|
|||
altText={person.name}
|
||||
title={person.name}
|
||||
widthStyle="100%"
|
||||
circle
|
||||
/>
|
||||
{#if person.isFavorite}
|
||||
<div class="absolute top-4 left-4">
|
||||
|
@ -70,14 +61,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if person.name}
|
||||
<span
|
||||
class="text-white-shadow absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white"
|
||||
title={person.name}
|
||||
>
|
||||
{person.name}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
{#if showVerticalDots}
|
||||
|
@ -91,7 +74,6 @@
|
|||
title={$t('show_person_options')}
|
||||
>
|
||||
<MenuOption onClick={onHidePerson} icon={mdiEyeOffOutline} text={$t('hide_person')} />
|
||||
<MenuOption onClick={onChangeName} icon={mdiAccountEditOutline} text={$t('change_name')} />
|
||||
<MenuOption onClick={onSetBirthDate} icon={mdiCalendarEditOutline} text={$t('set_date_of_birth')} />
|
||||
<MenuOption onClick={onMergePeople} icon={mdiAccountMultipleCheckOutline} text={$t('merge_people')} />
|
||||
<MenuOption
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1">
|
||||
<div class="w-full grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-10 gap-1">
|
||||
{#each people as person, index (person.id)}
|
||||
{#if hasNextPage && index === people.length - 1}
|
||||
<div bind:this={lastPersonContainer}>
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||
import SetBirthDateModal from '$lib/components/faces-page/set-birth-date-modal.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
|
@ -46,10 +45,9 @@
|
|||
|
||||
let selectHidden = $state(false);
|
||||
let searchName = $state('');
|
||||
let showChangeNameModal = $state(false);
|
||||
let showSetBirthDateModal = $state(false);
|
||||
let showMergeModal = $state(false);
|
||||
let personName = $state('');
|
||||
let newName = $state('');
|
||||
let currentPage = $state(1);
|
||||
let nextPage = $state(data.people.hasNextPage ? 2 : null);
|
||||
let personMerge1 = $state<PersonResponseDto>();
|
||||
|
@ -57,10 +55,9 @@
|
|||
let potentialMergePeople: PersonResponseDto[] = $state([]);
|
||||
let edittingPerson: PersonResponseDto | null = $state(null);
|
||||
let searchedPeopleLocal: PersonResponseDto[] = $state([]);
|
||||
// let handleSearchPeople: (force?: boolean, name?: string) => Promise<void> = $state();
|
||||
let changeNameInputEl = $state<HTMLInputElement>();
|
||||
let innerHeight = $state(0);
|
||||
let searchPeopleElement = $state<ReturnType<typeof SearchPeople>>();
|
||||
|
||||
onMount(() => {
|
||||
const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE);
|
||||
if (getSearchedPeople) {
|
||||
|
@ -158,7 +155,7 @@
|
|||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_save_name'));
|
||||
}
|
||||
if (personToBeMergedIn.name !== personName && edittingPerson.id === personToBeMergedIn.id) {
|
||||
if (personToBeMergedIn.name !== newName && 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
|
||||
|
@ -166,11 +163,11 @@
|
|||
*
|
||||
*/
|
||||
try {
|
||||
await updatePerson({ id: personToBeMergedIn.id, personUpdateDto: { name: personName } });
|
||||
await updatePerson({ id: personToBeMergedIn.id, personUpdateDto: { name: newName } });
|
||||
|
||||
for (const person of people) {
|
||||
if (person.id === personToBeMergedIn.id) {
|
||||
person.name = personName;
|
||||
person.name = newName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -184,15 +181,6 @@
|
|||
}
|
||||
};
|
||||
|
||||
const handleChangeName = (detail: PersonResponseDto) => {
|
||||
showChangeNameModal = true;
|
||||
personName = detail.name;
|
||||
personMerge1 = detail;
|
||||
edittingPerson = detail;
|
||||
|
||||
setTimeout(() => changeNameInputEl?.focus(), 100);
|
||||
};
|
||||
|
||||
const handleSetBirthDate = (detail: PersonResponseDto) => {
|
||||
showSetBirthDateModal = true;
|
||||
edittingPerson = detail;
|
||||
|
@ -212,7 +200,6 @@
|
|||
return person;
|
||||
});
|
||||
|
||||
showChangeNameModal = false;
|
||||
notificationController.show({
|
||||
message: $t('changed_visibility_successfully'),
|
||||
type: NotificationType.Info,
|
||||
|
@ -247,46 +234,6 @@
|
|||
);
|
||||
};
|
||||
|
||||
const submitNameChange = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
potentialMergePeople = [];
|
||||
showChangeNameModal = false;
|
||||
if (!edittingPerson || personName === edittingPerson.name) {
|
||||
return;
|
||||
}
|
||||
if (personName === '') {
|
||||
await changeName();
|
||||
return;
|
||||
}
|
||||
const data = await searchPerson({ name: personName, withHidden: true });
|
||||
|
||||
// We check if another person has the same name as the name entered by the user
|
||||
|
||||
const existingPerson = data.find(
|
||||
(person: PersonResponseDto) =>
|
||||
person.name.toLowerCase() === personName.toLowerCase() &&
|
||||
edittingPerson &&
|
||||
person.id !== edittingPerson.id &&
|
||||
person.name,
|
||||
);
|
||||
if (existingPerson) {
|
||||
personMerge2 = existingPerson;
|
||||
showMergeModal = true;
|
||||
potentialMergePeople = people
|
||||
.filter(
|
||||
(person: PersonResponseDto) =>
|
||||
personMerge2?.name.toLowerCase() === person.name.toLowerCase() &&
|
||||
person.id !== personMerge2.id &&
|
||||
person.id !== personMerge1?.id &&
|
||||
!person.isHidden,
|
||||
)
|
||||
.slice(0, 3);
|
||||
return;
|
||||
}
|
||||
await changeName();
|
||||
};
|
||||
|
||||
const submitBirthDateChange = async (value: string) => {
|
||||
showSetBirthDateModal = false;
|
||||
if (!edittingPerson || value === edittingPerson.birthDate) {
|
||||
|
@ -314,33 +261,6 @@
|
|||
}
|
||||
};
|
||||
|
||||
const changeName = async () => {
|
||||
showMergeModal = false;
|
||||
showChangeNameModal = false;
|
||||
|
||||
if (!edittingPerson) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const updatedPerson = await updatePerson({
|
||||
id: edittingPerson.id,
|
||||
personUpdateDto: { name: personName },
|
||||
});
|
||||
people = people.map((person: PersonResponseDto) => {
|
||||
if (person.id === updatedPerson.id) {
|
||||
return updatedPerson;
|
||||
}
|
||||
return person;
|
||||
});
|
||||
notificationController.show({
|
||||
message: $t('change_name_successfully'),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_save_name'));
|
||||
}
|
||||
};
|
||||
|
||||
const onResetSearchBar = async () => {
|
||||
await clearQueryParam(QueryParameter.SEARCHED_PEOPLE, $page.url);
|
||||
};
|
||||
|
@ -353,12 +273,74 @@
|
|||
let countVisiblePeople = $derived(searchName ? searchedPeopleLocal.length : data.people.total - data.people.hidden);
|
||||
let showPeople = $derived(searchName ? searchedPeopleLocal : visiblePeople);
|
||||
|
||||
// const submitNameChange = (event: Event) => {
|
||||
// event.preventDefault();
|
||||
// if (searchPeopleElement) {
|
||||
// handlePromiseError(searchPeopleElement.searchPeople(true, searchName));
|
||||
// }
|
||||
// };
|
||||
const onNameChangeInputFocus = (person: PersonResponseDto) => {
|
||||
edittingPerson = person;
|
||||
newName = person.name;
|
||||
};
|
||||
|
||||
const onNameChangeSubmit = async (name: string, targetPerson: PersonResponseDto) => {
|
||||
try {
|
||||
if (name == targetPerson.name || showMergeModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === '') {
|
||||
await updateName(targetPerson.id, '');
|
||||
return;
|
||||
}
|
||||
|
||||
const personWithSimilarName = await findPeopleWithSimilarName(name, targetPerson.id);
|
||||
if (personWithSimilarName) {
|
||||
personMerge1 = targetPerson;
|
||||
personMerge2 = personWithSimilarName;
|
||||
potentialMergePeople = people
|
||||
.filter(
|
||||
(person: PersonResponseDto) =>
|
||||
personMerge2?.name.toLowerCase() === person.name.toLowerCase() &&
|
||||
person.id !== personMerge2.id &&
|
||||
person.id !== personMerge1?.id &&
|
||||
!person.isHidden,
|
||||
)
|
||||
.slice(0, 3);
|
||||
showMergeModal = true;
|
||||
return;
|
||||
}
|
||||
await updateName(targetPerson.id, name);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_save_name'));
|
||||
}
|
||||
};
|
||||
|
||||
const onNameChangeInputUpdate = (event: Event) => {
|
||||
if (event.target) {
|
||||
newName = (event.target as HTMLInputElement).value;
|
||||
}
|
||||
};
|
||||
|
||||
const updateName = async (id: string, name: string) => {
|
||||
await updatePerson({
|
||||
id,
|
||||
personUpdateDto: { name },
|
||||
});
|
||||
|
||||
newName = '';
|
||||
};
|
||||
|
||||
const findPeopleWithSimilarName = async (name: string, personId: string) => {
|
||||
const searchResult = await searchPerson({ name, withHidden: true });
|
||||
return searchResult.find(
|
||||
(person) => person.name.toLowerCase() === name.toLowerCase() && person.id !== personId && person.name,
|
||||
);
|
||||
};
|
||||
|
||||
const handleMergeCancel = async () => {
|
||||
if (!personMerge1) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateName(personMerge1.id, newName);
|
||||
showMergeModal = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight />
|
||||
|
@ -368,8 +350,10 @@
|
|||
{personMerge1}
|
||||
{personMerge2}
|
||||
{potentialMergePeople}
|
||||
onClose={() => (showMergeModal = false)}
|
||||
onReject={changeName}
|
||||
onClose={() => {
|
||||
showMergeModal = false;
|
||||
}}
|
||||
onReject={() => handleMergeCancel()}
|
||||
onConfirm={handleMergeSamePerson}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -425,15 +409,30 @@
|
|||
{#if countVisiblePeople > 0 && (!searchName || searchedPeopleLocal.length > 0)}
|
||||
<PeopleInfiniteScroll people={showPeople} hasNextPage={!!nextPage && !searchName} {loadNextPage}>
|
||||
{#snippet children({ person, index })}
|
||||
<PeopleCard
|
||||
{person}
|
||||
preload={index < 20}
|
||||
onChangeName={() => handleChangeName(person)}
|
||||
onSetBirthDate={() => handleSetBirthDate(person)}
|
||||
onMergePeople={() => handleMergePeople(person)}
|
||||
onHidePerson={() => handleHidePerson(person)}
|
||||
onToggleFavorite={() => handleToggleFavorite(person)}
|
||||
/>
|
||||
<div
|
||||
class="p-2 rounded-xl hover:bg-gray-200 border-2 hover:border-immich-primary/50 hover:shadow-sm dark:hover:bg-immich-dark-primary/20 hover:dark:border-immich-dark-primary/25 border-transparent transition-all"
|
||||
>
|
||||
<PeopleCard
|
||||
{person}
|
||||
preload={index < 20}
|
||||
onSetBirthDate={() => handleSetBirthDate(person)}
|
||||
onMergePeople={() => handleMergePeople(person)}
|
||||
onHidePerson={() => handleHidePerson(person)}
|
||||
onToggleFavorite={() => handleToggleFavorite(person)}
|
||||
/>
|
||||
|
||||
<form onsubmit={() => onNameChangeSubmit(newName, person)}>
|
||||
<input
|
||||
type="text"
|
||||
class=" bg-white dark:bg-immich-dark-gray border-gray-100 placeholder-gray-400 text-center dark:border-gray-900 w-full rounded-2xl mt-2 py-2 text-sm text-immich-primary dark:text-immich-dark-primary"
|
||||
value={person.name}
|
||||
placeholder={$t('add_a_name')}
|
||||
onfocusin={() => onNameChangeInputFocus(person)}
|
||||
onfocusout={() => onNameChangeSubmit(newName, person)}
|
||||
oninput={(event) => onNameChangeInputUpdate(event)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
{/snippet}
|
||||
</PeopleInfiniteScroll>
|
||||
{:else}
|
||||
|
@ -447,35 +446,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showChangeNameModal}
|
||||
<FullScreenModal title={$t('change_name')} onClose={() => (showChangeNameModal = false)}>
|
||||
<form onsubmit={submitNameChange} autocomplete="off" id="change-name-form">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="name">{$t('name')}</label>
|
||||
<input
|
||||
class="immich-form-input"
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
bind:value={personName}
|
||||
bind:this={changeNameInputEl}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{#snippet stickyBottom()}
|
||||
<Button
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onclick={() => {
|
||||
showChangeNameModal = false;
|
||||
}}>{$t('cancel')}</Button
|
||||
>
|
||||
<Button type="submit" fullWidth form="change-name-form">{$t('ok')}</Button>
|
||||
{/snippet}
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
||||
{#if showSetBirthDateModal}
|
||||
<SetBirthDateModal
|
||||
birthDate={edittingPerson?.birthDate ?? ''}
|
||||
|
|
Loading…
Add table
Reference in a new issue