From 34bb6e4b15f4d60edbad58ad92a341075832038f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Proch=C3=A1zka?= Date: Mon, 15 Sep 2025 22:12:54 +0200 Subject: [PATCH 1/8] feat: add view for all selected people when merging faces --- i18n/cs.json | 1 + i18n/en.json | 1 + .../faces-page/merge-face-selector.svelte | 41 ++++++++++++++----- .../components/faces-page/people-list.svelte | 6 +-- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/i18n/cs.json b/i18n/cs.json index 7205926696..f3f4c905b7 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -1950,6 +1950,7 @@ "show_album_options": "Zobrazit možnosti alba", "show_albums": "Zobrazit alba", "show_all_people": "Zobrazit všechny lidi", + "show_all_selected_people": "Zobrazit všechny vybrané lidi", "show_and_hide_people": "Zobrazit a skrýt osoby", "show_file_location": "Zobrazit umístění souboru", "show_gallery": "Zobrazit galerii", diff --git a/i18n/en.json b/i18n/en.json index 42965e06a8..657c484e0a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1962,6 +1962,7 @@ "show_album_options": "Show album options", "show_albums": "Show albums", "show_all_people": "Show all people", + "show_all_selected_people": "Show all selected people", "show_and_hide_people": "Show & hide people", "show_file_location": "Show file location", "show_gallery": "Show gallery", diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index efec16173b..2bda448e54 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -5,7 +5,7 @@ import { handleError } from '$lib/utils/handle-error'; import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk'; import { Button, Icon, IconButton, modalManager, toastManager } from '@immich/ui'; - import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js'; + import { mdiCallMerge, mdiMerge, mdiPlus, mdiSwapHorizontal } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import { flip } from 'svelte/animate'; @@ -26,6 +26,7 @@ let people: PersonResponseDto[] = $state([]); let selectedPeople: PersonResponseDto[] = $state([]); let screenHeight: number = $state(0); + let allPeopleViewShown: boolean = $state(false); let hasSelection = $derived(selectedPeople.length > 0); let peopleToNotShow = $derived([...selectedPeople, person]); @@ -43,17 +44,20 @@ await goto(`${AppRoute.PEOPLE}/${person.id}?${page.url.searchParams.toString()}`); }; + const openAllPeopleView = () => { + allPeopleViewShown = true; + }; + + const closeAllPeopleView = () => { + allPeopleViewShown = false; + }; + const onSelect = async (selected: PersonResponseDto) => { if (selectedPeople.includes(selected)) { selectedPeople = selectedPeople.filter((person) => person.id !== selected.id); return; } - if (selectedPeople.length >= 5) { - toastManager.warning($t('merge_people_limit')); - return; - } - selectedPeople = [selected, ...selectedPeople]; if (selectedPeople.length === 1 && !person.name && selected.name) { @@ -88,7 +92,7 @@ transition:fly={{ y: 500, duration: 100, easing: quintOut }} class="absolute start-0 top-0 h-full w-full bg-light" > - + {#snippet leading()} {#if hasSelection} {$t('selected_count', { values: { count: selectedPeople.length } })} @@ -104,14 +108,16 @@ {/snippet}
-
+ +
diff --git a/web/src/lib/components/faces-page/people-list.svelte b/web/src/lib/components/faces-page/people-list.svelte index 7a401fefa3..ce8558bec1 100644 --- a/web/src/lib/components/faces-page/people-list.svelte +++ b/web/src/lib/components/faces-page/people-list.svelte @@ -16,7 +16,7 @@ let { screenHeight, people, peopleToNotShow, onSelect, handleSearch }: Props = $props(); let searchedPeopleLocal: PersonResponseDto[] = $state([]); - let sortBySimilarirty = $state(false); + let sortBySimilarity = $state(false); let name = $state(''); const showPeople = $derived( @@ -38,8 +38,8 @@ variant="ghost" icon={mdiSwapVertical} onclick={() => { - sortBySimilarirty = !sortBySimilarirty; - handleSearch(sortBySimilarirty); + sortBySimilarity = !sortBySimilarity; + handleSearch(sortBySimilarity); }} aria-label={$t('sort_people_by_similarity')} /> From a25a4b97d52f7c0312d2791cfa06ba4b26108eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Proch=C3=A1zka?= Date: Mon, 15 Sep 2025 22:39:11 +0200 Subject: [PATCH 2/8] show merge button only on main face merge screen --- .../lib/components/faces-page/merge-face-selector.svelte | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index 2bda448e54..d8cc2b4ca6 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -102,9 +102,11 @@
{/snippet} {#snippet trailing()} - + {#if !allPeopleViewShown} + + {/if} {/snippet}
From 033315c959d170eb78be599011ab9aba20e3f79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Proch=C3=A1zka?= Date: Sat, 29 Nov 2025 16:04:00 +0100 Subject: [PATCH 3/8] use Modal for all people view --- i18n/cs.json | 1 - .../faces-page/merge-face-selector.svelte | 42 ++++++++----------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/i18n/cs.json b/i18n/cs.json index f3f4c905b7..7205926696 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -1950,7 +1950,6 @@ "show_album_options": "Zobrazit možnosti alba", "show_albums": "Zobrazit alba", "show_all_people": "Zobrazit všechny lidi", - "show_all_selected_people": "Zobrazit všechny vybrané lidi", "show_and_hide_people": "Zobrazit a skrýt osoby", "show_file_location": "Zobrazit umístění souboru", "show_gallery": "Zobrazit galerii", diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index d8cc2b4ca6..e19a41886a 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -4,7 +4,7 @@ import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants'; import { handleError } from '$lib/utils/handle-error'; import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk'; - import { Button, Icon, IconButton, modalManager, toastManager } from '@immich/ui'; + import { Button, Icon, IconButton, Modal, ModalBody, modalManager, toastManager } from '@immich/ui'; import { mdiCallMerge, mdiMerge, mdiPlus, mdiSwapHorizontal } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -26,7 +26,7 @@ let people: PersonResponseDto[] = $state([]); let selectedPeople: PersonResponseDto[] = $state([]); let screenHeight: number = $state(0); - let allPeopleViewShown: boolean = $state(false); + let allPeopleViewModalOpen: boolean = $state(false); let hasSelection = $derived(selectedPeople.length > 0); let peopleToNotShow = $derived([...selectedPeople, person]); @@ -44,14 +44,6 @@ await goto(`${AppRoute.PEOPLE}/${person.id}?${page.url.searchParams.toString()}`); }; - const openAllPeopleView = () => { - allPeopleViewShown = true; - }; - - const closeAllPeopleView = () => { - allPeopleViewShown = false; - }; - const onSelect = async (selected: PersonResponseDto) => { if (selectedPeople.includes(selected)) { selectedPeople = selectedPeople.filter((person) => person.id !== selected.id); @@ -92,7 +84,7 @@ transition:fly={{ y: 500, duration: 100, easing: quintOut }} class="absolute start-0 top-0 h-full w-full bg-light" > - + {#snippet leading()} {#if hasSelection} {$t('selected_count', { values: { count: selectedPeople.length } })} @@ -102,24 +94,20 @@
{/snippet} {#snippet trailing()} - {#if !allPeopleViewShown} - - {/if} + {/snippet}
-
From b8b28cee8cd8f32928e7b0d29271d84aace4a729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Proch=C3=A1zka?= Date: Sat, 29 Nov 2025 16:07:43 +0100 Subject: [PATCH 4/8] remove obsolete message --- i18n/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/i18n/en.json b/i18n/en.json index 657c484e0a..681fb8bd13 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1393,7 +1393,6 @@ "menu": "Menu", "merge": "Merge", "merge_people": "Merge people", - "merge_people_limit": "You can only merge up to 5 faces at a time", "merge_people_prompt": "Do you want to merge these people? This action is irreversible.", "merge_people_successfully": "Merge people successfully", "merged_people_count": "Merged {count, plural, one {# person} other {# people}}", From dbe1350f77f94df6699a4e9e5520779261482779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Proch=C3=A1zka?= Date: Sat, 29 Nov 2025 17:13:05 +0100 Subject: [PATCH 5/8] remove search bar from view of all selected people --- .../lib/components/faces-page/merge-face-selector.svelte | 4 ++-- web/src/lib/components/faces-page/people-list.svelte | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index e19a41886a..6f84df0ebe 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -122,7 +122,7 @@ color="secondary" aria-label={$t('show_all_selected_people')} icon={mdiPlus} - size="large" + size="medium" onclick={() => allPeopleViewModalOpen = true} /> @@ -152,7 +152,7 @@ {#if allPeopleViewModalOpen} allPeopleViewModalOpen = false}> - + {/if} diff --git a/web/src/lib/components/faces-page/people-list.svelte b/web/src/lib/components/faces-page/people-list.svelte index ce8558bec1..6e8fb2f4c2 100644 --- a/web/src/lib/components/faces-page/people-list.svelte +++ b/web/src/lib/components/faces-page/people-list.svelte @@ -27,11 +27,11 @@
-
- -
- {#if handleSearch} +
+ +
+ Date: Sat, 29 Nov 2025 17:28:13 +0100 Subject: [PATCH 6/8] change modal title and add modal body text --- i18n/en.json | 2 ++ web/src/lib/components/faces-page/merge-face-selector.svelte | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/i18n/en.json b/i18n/en.json index 681fb8bd13..88a6bbe0cf 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -716,6 +716,7 @@ "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "check_logs": "Check Logs", "choose_matching_people_to_merge": "Choose matching people to merge", + "choose_people_to_unselect": "Choose people to unselect", "city": "City", "clear": "Clear", "clear_all": "Clear all", @@ -1845,6 +1846,7 @@ "selected": "Selected", "selected_count": "{count, plural, other {# selected}}", "selected_gps_coordinates": "Selected GPS Coordinates", + "selected_people_to_merge": "Selected people to merge", "send_message": "Send message", "send_welcome_email": "Send welcome email", "server_endpoint": "Server Endpoint", diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index 6f84df0ebe..9561fba191 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -150,8 +150,9 @@
{#if allPeopleViewModalOpen} - allPeopleViewModalOpen = false}> + allPeopleViewModalOpen = false}> +

{$t('choose_people_to_unselect')}

From 833b231a8af9c896672b83ba5525f5b0a8d7928b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Proch=C3=A1zka?= Date: Mon, 1 Dec 2025 21:39:11 +0100 Subject: [PATCH 7/8] refactor into separate component --- .../faces-page/merge-face-selector.svelte | 19 +++++++-------- web/src/lib/modals/PeopleViewModal.svelte | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 web/src/lib/modals/PeopleViewModal.svelte diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index 9561fba191..292fdb8624 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -4,7 +4,7 @@ import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants'; import { handleError } from '$lib/utils/handle-error'; import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk'; - import { Button, Icon, IconButton, Modal, ModalBody, modalManager, toastManager } from '@immich/ui'; + import { Button, Icon, IconButton, modalManager, toastManager } from '@immich/ui'; import { mdiCallMerge, mdiMerge, mdiPlus, mdiSwapHorizontal } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -14,6 +14,7 @@ import ControlAppBar from '../shared-components/control-app-bar.svelte'; import FaceThumbnail from './face-thumbnail.svelte'; import PeopleList from './people-list.svelte'; + import PeopleViewModal from "$lib/modals/PeopleViewModal.svelte"; interface Props { person: PersonResponseDto; @@ -26,7 +27,6 @@ let people: PersonResponseDto[] = $state([]); let selectedPeople: PersonResponseDto[] = $state([]); let screenHeight: number = $state(0); - let allPeopleViewModalOpen: boolean = $state(false); let hasSelection = $derived(selectedPeople.length > 0); let peopleToNotShow = $derived([...selectedPeople, person]); @@ -123,7 +123,12 @@ aria-label={$t('show_all_selected_people')} icon={mdiPlus} size="medium" - onclick={() => allPeopleViewModalOpen = true} + onclick={() => modalManager.show(PeopleViewModal, { + people: peopleToNotShow, + peopleToNotShow: [person], + screenHeight, + onSelect + })} /> {/if} @@ -149,14 +154,6 @@ - {#if allPeopleViewModalOpen} - allPeopleViewModalOpen = false}> - -

{$t('choose_people_to_unselect')}

- -
-
- {/if}
diff --git a/web/src/lib/modals/PeopleViewModal.svelte b/web/src/lib/modals/PeopleViewModal.svelte new file mode 100644 index 0000000000..042a911170 --- /dev/null +++ b/web/src/lib/modals/PeopleViewModal.svelte @@ -0,0 +1,24 @@ + + + + +

{$t('choose_people_to_unselect')}

+ +
+
From 727bb6ccdc6567751859d62f321ad745abd909a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Proch=C3=A1zka?= Date: Fri, 5 Dec 2025 00:18:02 +0100 Subject: [PATCH 8/8] return updated selectedPeople with onClose --- .../faces-page/merge-face-selector.svelte | 16 ++++++++++------ web/src/lib/modals/PeopleViewModal.svelte | 15 +++++++++++---- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index 292fdb8624..92ecfb8f77 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -76,6 +76,15 @@ handleError(error, $t('cannot_merge_people')); } }; + + const showPeopleViewModal = async () => { + // Sets the selected people that remained after closing the modal. + selectedPeople = await modalManager.show(PeopleViewModal, { + people: selectedPeople, + peopleToNotShow: [person], + screenHeight + }); + }; @@ -123,12 +132,7 @@ aria-label={$t('show_all_selected_people')} icon={mdiPlus} size="medium" - onclick={() => modalManager.show(PeopleViewModal, { - people: peopleToNotShow, - peopleToNotShow: [person], - screenHeight, - onSelect - })} + onclick={showPeopleViewModal} /> {/if} diff --git a/web/src/lib/modals/PeopleViewModal.svelte b/web/src/lib/modals/PeopleViewModal.svelte index 042a911170..48bd14770d 100644 --- a/web/src/lib/modals/PeopleViewModal.svelte +++ b/web/src/lib/modals/PeopleViewModal.svelte @@ -8,15 +8,22 @@ people: PersonResponseDto[]; peopleToNotShow: PersonResponseDto[]; screenHeight: number; - onSelect: (person: PersonResponseDto) => void; - onClose: () => void; + onClose: (people: PersonResponseDto[]) => void; } - let { people, peopleToNotShow, screenHeight, onSelect, onClose }: Props = $props(); + let { people, peopleToNotShow, screenHeight, onClose }: Props = $props(); + + // Hides the selected person. + const onSelect = (selected: PersonResponseDto) => { + if (people.includes(selected)) { + people = people.filter((person) => person.id !== selected.id); + return; + } + }; - + onClose(people)}>

{$t('choose_people_to_unselect')}