mirror of
https://github.com/immich-app/immich.git
synced 2025-06-04 22:24:26 -04:00
refactor: person merge suggestion modal (#18287)
This commit is contained in:
parent
117b263887
commit
cd03d0c0f2
@ -1,126 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
|
||||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
|
||||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
|
||||||
import { type PersonResponseDto } from '@immich/sdk';
|
|
||||||
import { Button } from '@immich/ui';
|
|
||||||
import { mdiArrowLeft, mdiMerge } from '@mdi/js';
|
|
||||||
import { onMount, tick } from 'svelte';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
|
||||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
personMerge1: PersonResponseDto;
|
|
||||||
personMerge2: PersonResponseDto;
|
|
||||||
potentialMergePeople: PersonResponseDto[];
|
|
||||||
onReject: () => void;
|
|
||||||
onConfirm: ([personMerge1, personMerge2]: [PersonResponseDto, PersonResponseDto]) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
personMerge1 = $bindable(),
|
|
||||||
personMerge2 = $bindable(),
|
|
||||||
potentialMergePeople = $bindable(),
|
|
||||||
onReject,
|
|
||||||
onConfirm,
|
|
||||||
onClose,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let choosePersonToMerge = $state(false);
|
|
||||||
|
|
||||||
const title = personMerge2.name;
|
|
||||||
|
|
||||||
const changePersonToMerge = (newPerson: PersonResponseDto) => {
|
|
||||||
const index = potentialMergePeople.indexOf(newPerson);
|
|
||||||
[potentialMergePeople[index], personMerge2] = [personMerge2, potentialMergePeople[index]];
|
|
||||||
choosePersonToMerge = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await tick();
|
|
||||||
document.querySelector<HTMLElement>('#merge-confirm-button')?.focus();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<FullScreenModal title="{$t('merge_people')} - {title}" {onClose}>
|
|
||||||
<div class="flex items-center justify-center py-4 md:h-36 md:py-4">
|
|
||||||
{#if !choosePersonToMerge}
|
|
||||||
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
|
|
||||||
<ImageThumbnail
|
|
||||||
circle
|
|
||||||
shadow
|
|
||||||
url={getPeopleThumbnailUrl(personMerge1)}
|
|
||||||
altText={personMerge1.name}
|
|
||||||
widthStyle="100%"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mx-0.5 flex md:mx-2">
|
|
||||||
<CircleIconButton
|
|
||||||
title={$t('swap_merge_direction')}
|
|
||||||
icon={mdiMerge}
|
|
||||||
onclick={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={potentialMergePeople.length === 0}
|
|
||||||
class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
|
|
||||||
onclick={() => {
|
|
||||||
if (potentialMergePeople.length > 0) {
|
|
||||||
choosePersonToMerge = !choosePersonToMerge;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ImageThumbnail
|
|
||||||
border={potentialMergePeople.length > 0}
|
|
||||||
circle
|
|
||||||
shadow
|
|
||||||
url={getPeopleThumbnailUrl(personMerge2)}
|
|
||||||
altText={personMerge2.name}
|
|
||||||
widthStyle="100%"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<div class="grid w-full grid-cols-1 gap-2">
|
|
||||||
<div class="px-2">
|
|
||||||
<button type="button" onclick={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
|
|
||||||
{#each potentialMergePeople as person (person.id)}
|
|
||||||
<div class="h-24 w-24 md:h-28 md:w-28">
|
|
||||||
<button type="button" class="p-2 w-full" onclick={() => changePersonToMerge(person)}>
|
|
||||||
<ImageThumbnail
|
|
||||||
border={true}
|
|
||||||
circle
|
|
||||||
shadow
|
|
||||||
url={getPeopleThumbnailUrl(person)}
|
|
||||||
altText={person.name}
|
|
||||||
widthStyle="100%"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex px-4 md:pt-4">
|
|
||||||
<h1 class="text-xl text-gray-500 dark:text-gray-300">{$t('are_these_the_same_person')}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="flex px-4 pt-2">
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-300">{$t('they_will_be_merged_together')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#snippet stickyBottom()}
|
|
||||||
<Button fullWidth shape="round" color="secondary" onclick={onReject}>{$t('no')}</Button>
|
|
||||||
<Button id="merge-confirm-button" fullWidth shape="round" onclick={() => onConfirm([personMerge1, personMerge2])}>
|
|
||||||
{$t('yes')}
|
|
||||||
</Button>
|
|
||||||
{/snippet}
|
|
||||||
</FullScreenModal>
|
|
@ -383,7 +383,6 @@ export enum PersonPageViewMode {
|
|||||||
VIEW_ASSETS = 'view-assets',
|
VIEW_ASSETS = 'view-assets',
|
||||||
SELECT_PERSON = 'select-person',
|
SELECT_PERSON = 'select-person',
|
||||||
MERGE_PEOPLE = 'merge-people',
|
MERGE_PEOPLE = 'merge-people',
|
||||||
SUGGEST_MERGE = 'suggest-merge',
|
|
||||||
UNASSIGN_ASSETS = 'unassign-faces',
|
UNASSIGN_ASSETS = 'unassign-faces',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
147
web/src/lib/modals/PersonMergeSuggestionModal.svelte
Normal file
147
web/src/lib/modals/PersonMergeSuggestionModal.svelte
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
import {
|
||||||
|
notificationController,
|
||||||
|
NotificationType,
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { mergePerson, type PersonResponseDto } from '@immich/sdk';
|
||||||
|
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
|
||||||
|
import { mdiArrowLeft, mdiMerge } from '@mdi/js';
|
||||||
|
import { onMount, tick } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import ImageThumbnail from '../components/assets/thumbnail/image-thumbnail.svelte';
|
||||||
|
import CircleIconButton from '../components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
personToMerge: PersonResponseDto;
|
||||||
|
personToBeMergedInto: PersonResponseDto;
|
||||||
|
potentialMergePeople: PersonResponseDto[];
|
||||||
|
onClose: (people?: [PersonResponseDto, PersonResponseDto]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
personToMerge = $bindable(),
|
||||||
|
personToBeMergedInto = $bindable(),
|
||||||
|
potentialMergePeople = $bindable(),
|
||||||
|
onClose,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let choosePersonToMerge = $state(false);
|
||||||
|
|
||||||
|
const title = personToBeMergedInto.name;
|
||||||
|
|
||||||
|
const changePersonToMerge = (newPerson: PersonResponseDto) => {
|
||||||
|
const index = potentialMergePeople.indexOf(newPerson);
|
||||||
|
[potentialMergePeople[index], personToBeMergedInto] = [personToBeMergedInto, potentialMergePeople[index]];
|
||||||
|
choosePersonToMerge = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMergePerson = async () => {
|
||||||
|
try {
|
||||||
|
await mergePerson({
|
||||||
|
id: personToBeMergedInto.id,
|
||||||
|
mergePersonDto: { ids: [personToMerge.id] },
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationController.show({
|
||||||
|
message: $t('merge_people_successfully'),
|
||||||
|
type: NotificationType.Info,
|
||||||
|
});
|
||||||
|
onClose([personToMerge, personToBeMergedInto]);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_save_name'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await tick();
|
||||||
|
document.querySelector<HTMLElement>('#merge-confirm-button')?.focus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal title="{$t('merge_people')} - {title}" {onClose}>
|
||||||
|
<ModalBody>
|
||||||
|
<div class="flex items-center justify-center py-4 md:h-36 md:py-4">
|
||||||
|
{#if !choosePersonToMerge}
|
||||||
|
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
|
||||||
|
<ImageThumbnail
|
||||||
|
circle
|
||||||
|
shadow
|
||||||
|
url={getPeopleThumbnailUrl(personToMerge)}
|
||||||
|
altText={personToMerge.name}
|
||||||
|
widthStyle="100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mx-0.5 flex md:mx-2">
|
||||||
|
<CircleIconButton
|
||||||
|
title={$t('swap_merge_direction')}
|
||||||
|
icon={mdiMerge}
|
||||||
|
onclick={() => ([personToMerge, personToBeMergedInto] = [personToBeMergedInto, personToMerge])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={potentialMergePeople.length === 0}
|
||||||
|
class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
|
||||||
|
onclick={() => {
|
||||||
|
if (potentialMergePeople.length > 0) {
|
||||||
|
choosePersonToMerge = !choosePersonToMerge;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImageThumbnail
|
||||||
|
border={potentialMergePeople.length > 0}
|
||||||
|
circle
|
||||||
|
shadow
|
||||||
|
url={getPeopleThumbnailUrl(personToBeMergedInto)}
|
||||||
|
altText={personToBeMergedInto.name}
|
||||||
|
widthStyle="100%"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<div class="grid w-full grid-cols-1 gap-2">
|
||||||
|
<div class="px-2">
|
||||||
|
<button type="button" onclick={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
|
||||||
|
{#each potentialMergePeople as person (person.id)}
|
||||||
|
<div class="h-24 w-24 md:h-28 md:w-28">
|
||||||
|
<button type="button" class="p-2 w-full" onclick={() => changePersonToMerge(person)}>
|
||||||
|
<ImageThumbnail
|
||||||
|
border={true}
|
||||||
|
circle
|
||||||
|
shadow
|
||||||
|
url={getPeopleThumbnailUrl(person)}
|
||||||
|
altText={person.name}
|
||||||
|
widthStyle="100%"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex px-4 md:pt-4">
|
||||||
|
<h1 class="text-xl text-gray-500 dark:text-gray-300">{$t('are_these_the_same_person')}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex px-4 pt-2">
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-300">{$t('they_will_be_merged_together')}</p>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<div class="flex gap-3 w-full">
|
||||||
|
<Button fullWidth shape="round" color="secondary" onclick={() => onClose()}>{$t('no')}</Button>
|
||||||
|
<Button id="merge-confirm-button" fullWidth shape="round" onclick={handleMergePerson}>
|
||||||
|
{$t('yes')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
@ -3,9 +3,9 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { focusTrap } from '$lib/actions/focus-trap';
|
import { focusTrap } from '$lib/actions/focus-trap';
|
||||||
import { scrollMemory } from '$lib/actions/scroll-memory';
|
import { scrollMemory } from '$lib/actions/scroll-memory';
|
||||||
|
import { shortcut } from '$lib/actions/shortcut';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte';
|
import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte';
|
||||||
import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte';
|
|
||||||
import PeopleCard from '$lib/components/faces-page/people-card.svelte';
|
import PeopleCard from '$lib/components/faces-page/people-card.svelte';
|
||||||
import PeopleInfiniteScroll from '$lib/components/faces-page/people-infinite-scroll.svelte';
|
import PeopleInfiniteScroll from '$lib/components/faces-page/people-infinite-scroll.svelte';
|
||||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||||
@ -17,19 +17,13 @@
|
|||||||
import { ActionQueryParameterValue, AppRoute, QueryParameter, SessionStorageKey } from '$lib/constants';
|
import { ActionQueryParameterValue, AppRoute, QueryParameter, SessionStorageKey } from '$lib/constants';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import PersonEditBirthDateModal from '$lib/modals/PersonEditBirthDateModal.svelte';
|
import PersonEditBirthDateModal from '$lib/modals/PersonEditBirthDateModal.svelte';
|
||||||
|
import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { clearQueryParam } from '$lib/utils/navigation';
|
import { clearQueryParam } from '$lib/utils/navigation';
|
||||||
import {
|
import { getAllPeople, getPerson, searchPerson, updatePerson, type PersonResponseDto } from '@immich/sdk';
|
||||||
getAllPeople,
|
|
||||||
getPerson,
|
|
||||||
mergePerson,
|
|
||||||
searchPerson,
|
|
||||||
updatePerson,
|
|
||||||
type PersonResponseDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { Button } from '@immich/ui';
|
import { Button } from '@immich/ui';
|
||||||
import { mdiAccountOff, mdiEyeOutline } from '@mdi/js';
|
import { mdiAccountOff, mdiEyeOutline } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
@ -46,7 +40,6 @@
|
|||||||
|
|
||||||
let selectHidden = $state(false);
|
let selectHidden = $state(false);
|
||||||
let searchName = $state('');
|
let searchName = $state('');
|
||||||
let showMergeModal = $state(false);
|
|
||||||
let newName = $state('');
|
let newName = $state('');
|
||||||
let currentPage = $state(1);
|
let currentPage = $state(1);
|
||||||
let nextPage = $state(data.people.hasNextPage ? 2 : null);
|
let nextPage = $state(data.people.hasNextPage ? 2 : null);
|
||||||
@ -131,42 +124,41 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => {
|
const handleMerge = async () => {
|
||||||
const [personToMerge, personToBeMergedIn] = response;
|
if (!editingPerson || !personMerge1 || !personMerge2) {
|
||||||
showMergeModal = false;
|
|
||||||
|
|
||||||
if (!editingPerson) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await mergePerson({
|
const response = await modalManager.show(PersonMergeSuggestionModal, {
|
||||||
id: personToBeMergedIn.id,
|
personToMerge: personMerge1,
|
||||||
mergePersonDto: { ids: [personToMerge.id] },
|
personToBeMergedInto: personMerge2,
|
||||||
|
potentialMergePeople,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergedPerson = await getPerson({ id: personToBeMergedIn.id });
|
if (!response) {
|
||||||
|
await updateName(personMerge1.id, newName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [personToMerge, personToBeMergedInto] = response;
|
||||||
|
|
||||||
|
const mergedPerson = await getPerson({ id: personToBeMergedInto.id });
|
||||||
|
|
||||||
people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id);
|
people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id);
|
||||||
people = people.map((person: PersonResponseDto) => (person.id === personToBeMergedIn.id ? mergedPerson : person));
|
people = people.map((person: PersonResponseDto) => (person.id === personToBeMergedInto.id ? mergedPerson : person));
|
||||||
notificationController.show({
|
|
||||||
message: $t('merge_people_successfully'),
|
if (personToBeMergedInto.name !== newName && editingPerson.id === personToBeMergedInto.id) {
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_save_name'));
|
|
||||||
}
|
|
||||||
if (personToBeMergedIn.name !== newName && editingPerson.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
|
* If the user merges one of the suggested people into the person he's editing, it's merging the suggested person AND renames
|
||||||
* the person he's editing
|
* the person he's editing
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
try {
|
try {
|
||||||
await updatePerson({ id: personToBeMergedIn.id, personUpdateDto: { name: newName } });
|
await updatePerson({ id: personToBeMergedInto.id, personUpdateDto: { name: newName } });
|
||||||
|
|
||||||
for (const person of people) {
|
for (const person of people) {
|
||||||
if (person.id === personToBeMergedIn.id) {
|
if (person.id === personToBeMergedInto.id) {
|
||||||
person.name = newName;
|
person.name = newName;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -263,7 +255,7 @@
|
|||||||
|
|
||||||
const onNameChangeSubmit = async (name: string, targetPerson: PersonResponseDto) => {
|
const onNameChangeSubmit = async (name: string, targetPerson: PersonResponseDto) => {
|
||||||
try {
|
try {
|
||||||
if (name == targetPerson.name || showMergeModal) {
|
if (name == targetPerson.name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,7 +277,7 @@
|
|||||||
!person.isHidden,
|
!person.isHidden,
|
||||||
)
|
)
|
||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
showMergeModal = true;
|
await handleMerge();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await updateName(targetPerson.id, name);
|
await updateName(targetPerson.id, name);
|
||||||
@ -315,32 +307,10 @@
|
|||||||
(person) => person.name.toLowerCase() === name.toLowerCase() && person.id !== personId && person.name,
|
(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>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:innerHeight />
|
<svelte:window bind:innerHeight />
|
||||||
|
|
||||||
{#if showMergeModal && personMerge1 && personMerge2}
|
|
||||||
<MergeSuggestionModal
|
|
||||||
{personMerge1}
|
|
||||||
{personMerge2}
|
|
||||||
{potentialMergePeople}
|
|
||||||
onClose={() => {
|
|
||||||
showMergeModal = false;
|
|
||||||
}}
|
|
||||||
onReject={() => handleMergeCancel()}
|
|
||||||
onConfirm={handleMergeSamePerson}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<UserPageLayout
|
<UserPageLayout
|
||||||
title={$t('people')}
|
title={$t('people')}
|
||||||
description={countVisiblePeople === 0 && !searchName ? undefined : `(${countVisiblePeople.toLocaleString($locale)})`}
|
description={countVisiblePeople === 0 && !searchName ? undefined : `(${countVisiblePeople.toLocaleString($locale)})`}
|
||||||
@ -403,17 +373,16 @@
|
|||||||
onToggleFavorite={() => handleToggleFavorite(person)}
|
onToggleFavorite={() => handleToggleFavorite(person)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<form onsubmit={() => onNameChangeSubmit(newName, person)}>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
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"
|
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}
|
value={person.name}
|
||||||
placeholder={$t('add_a_name')}
|
placeholder={$t('add_a_name')}
|
||||||
|
use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: (e) => e.currentTarget.blur() }}
|
||||||
onfocusin={() => onNameChangeInputFocus(person)}
|
onfocusin={() => onNameChangeInputFocus(person)}
|
||||||
onfocusout={() => onNameChangeSubmit(newName, person)}
|
onfocusout={() => onNameChangeSubmit(newName, person)}
|
||||||
oninput={(event) => onNameChangeInputUpdate(event)}
|
oninput={(event) => onNameChangeInputUpdate(event)}
|
||||||
/>
|
/>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</PeopleInfiniteScroll>
|
</PeopleInfiniteScroll>
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||||
import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
|
import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
|
||||||
import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte';
|
import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte';
|
||||||
import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte';
|
|
||||||
import UnMergeFaceSelector from '$lib/components/faces-page/unmerge-face-selector.svelte';
|
import UnMergeFaceSelector from '$lib/components/faces-page/unmerge-face-selector.svelte';
|
||||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||||
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
|
||||||
@ -32,6 +31,7 @@
|
|||||||
import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
|
import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import PersonEditBirthDateModal from '$lib/modals/PersonEditBirthDateModal.svelte';
|
import PersonEditBirthDateModal from '$lib/modals/PersonEditBirthDateModal.svelte';
|
||||||
|
import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||||
@ -44,7 +44,6 @@
|
|||||||
import {
|
import {
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
getPersonStatistics,
|
getPersonStatistics,
|
||||||
mergePerson,
|
|
||||||
searchPerson,
|
searchPerson,
|
||||||
updatePerson,
|
updatePerson,
|
||||||
type AssetResponseDto,
|
type AssetResponseDto,
|
||||||
@ -122,7 +121,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleEscape = async () => {
|
const handleEscape = async () => {
|
||||||
if ($showAssetViewer || viewMode === PersonPageViewMode.SUGGEST_MERGE) {
|
if ($showAssetViewer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
@ -220,31 +219,32 @@
|
|||||||
viewMode = PersonPageViewMode.VIEW_ASSETS;
|
viewMode = PersonPageViewMode.VIEW_ASSETS;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => {
|
const handleMergeSuggestion = async () => {
|
||||||
const [personToMerge, personToBeMergedIn] = response;
|
if (!personMerge1 || !personMerge2) {
|
||||||
viewMode = PersonPageViewMode.VIEW_ASSETS;
|
return;
|
||||||
isEditingName = false;
|
}
|
||||||
try {
|
|
||||||
await mergePerson({
|
const result = await modalManager.show(PersonMergeSuggestionModal, {
|
||||||
id: personToBeMergedIn.id,
|
personToMerge: personMerge1,
|
||||||
mergePersonDto: { ids: [personToMerge.id] },
|
personToBeMergedInto: personMerge2,
|
||||||
});
|
potentialMergePeople,
|
||||||
notificationController.show({
|
|
||||||
message: $t('merge_people_successfully'),
|
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [personToMerge, personToBeMergedInto] = result;
|
||||||
|
|
||||||
people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id);
|
people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id);
|
||||||
if (personToBeMergedIn.name != personName && person.id === personToBeMergedIn.id) {
|
if (personToBeMergedInto.name != personName && person.id === personToBeMergedInto.id) {
|
||||||
await updateAssetCount();
|
await updateAssetCount();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await goto(`${AppRoute.PEOPLE}/${personToBeMergedIn.id}`, { replaceState: true });
|
await goto(`${AppRoute.PEOPLE}/${personToBeMergedInto.id}`, { replaceState: true });
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_save_name'));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSuggestPeople = (person2: PersonResponseDto) => {
|
const handleSuggestPeople = async (person2: PersonResponseDto) => {
|
||||||
isEditingName = false;
|
isEditingName = false;
|
||||||
if (person.id !== person2.id) {
|
if (person.id !== person2.id) {
|
||||||
potentialMergePeople = [];
|
potentialMergePeople = [];
|
||||||
@ -252,7 +252,8 @@
|
|||||||
personMerge1 = person;
|
personMerge1 = person;
|
||||||
personMerge2 = person2;
|
personMerge2 = person2;
|
||||||
isSuggestionSelectedByUser = true;
|
isSuggestionSelectedByUser = true;
|
||||||
viewMode = PersonPageViewMode.SUGGEST_MERGE;
|
|
||||||
|
await handleMergeSuggestion();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -280,9 +281,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelEditName = () => {
|
const handleCancelEditName = () => {
|
||||||
if (viewMode === PersonPageViewMode.SUGGEST_MERGE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isSearchingPeople = false;
|
isSearchingPeople = false;
|
||||||
isEditingName = false;
|
isEditingName = false;
|
||||||
};
|
};
|
||||||
@ -317,7 +315,7 @@
|
|||||||
!person.isHidden,
|
!person.isHidden,
|
||||||
)
|
)
|
||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
viewMode = PersonPageViewMode.SUGGEST_MERGE;
|
await handleMergeSuggestion();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await changeName();
|
await changeName();
|
||||||
@ -382,7 +380,7 @@
|
|||||||
onSelect={handleSelectFeaturePhoto}
|
onSelect={handleSelectFeaturePhoto}
|
||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
>
|
>
|
||||||
{#if viewMode === PersonPageViewMode.VIEW_ASSETS || viewMode === PersonPageViewMode.SUGGEST_MERGE}
|
{#if viewMode === PersonPageViewMode.VIEW_ASSETS}
|
||||||
<!-- Person information block -->
|
<!-- Person information block -->
|
||||||
<div
|
<div
|
||||||
class="relative w-fit p-4 sm:px-6"
|
class="relative w-fit p-4 sm:px-6"
|
||||||
@ -497,17 +495,6 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if viewMode === PersonPageViewMode.SUGGEST_MERGE && personMerge1 && personMerge2}
|
|
||||||
<MergeSuggestionModal
|
|
||||||
{personMerge1}
|
|
||||||
{personMerge2}
|
|
||||||
{potentialMergePeople}
|
|
||||||
onClose={() => (viewMode = PersonPageViewMode.VIEW_ASSETS)}
|
|
||||||
onReject={changeName}
|
|
||||||
onConfirm={handleMergeSamePerson}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if viewMode === PersonPageViewMode.MERGE_PEOPLE}
|
{#if viewMode === PersonPageViewMode.MERGE_PEOPLE}
|
||||||
<MergeFaceSelector {person} onBack={handleGoBack} onMerge={handleMerge} />
|
<MergeFaceSelector {person} onBack={handleGoBack} onMerge={handleMerge} />
|
||||||
{/if}
|
{/if}
|
||||||
@ -553,7 +540,7 @@
|
|||||||
</ButtonContextMenu>
|
</ButtonContextMenu>
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
{:else}
|
{:else}
|
||||||
{#if viewMode === PersonPageViewMode.VIEW_ASSETS || viewMode === PersonPageViewMode.SUGGEST_MERGE}
|
{#if viewMode === PersonPageViewMode.VIEW_ASSETS}
|
||||||
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(previousRoute)}>
|
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(previousRoute)}>
|
||||||
{#snippet trailing()}
|
{#snippet trailing()}
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user