bmondream 2025-12-10 17:15:12 +07:00 committed by GitHub
commit 81f2af8fd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 65 additions and 15 deletions

@ -721,6 +721,7 @@
"check_logs": "Check Logs", "check_logs": "Check Logs",
"checksum": "Checksum", "checksum": "Checksum",
"choose_matching_people_to_merge": "Choose matching people to merge", "choose_matching_people_to_merge": "Choose matching people to merge",
"choose_people_to_unselect": "Choose people to unselect",
"city": "City", "city": "City",
"clear": "Clear", "clear": "Clear",
"clear_all": "Clear all", "clear_all": "Clear all",
@ -1401,7 +1402,6 @@
"menu": "Menu", "menu": "Menu",
"merge": "Merge", "merge": "Merge",
"merge_people": "Merge people", "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_prompt": "Do you want to merge these people? This action is irreversible.",
"merge_people_successfully": "Merge people successfully", "merge_people_successfully": "Merge people successfully",
"merged_people_count": "Merged {count, plural, one {# person} other {# people}}", "merged_people_count": "Merged {count, plural, one {# person} other {# people}}",
@ -1854,6 +1854,7 @@
"selected": "Selected", "selected": "Selected",
"selected_count": "{count, plural, other {# selected}}", "selected_count": "{count, plural, other {# selected}}",
"selected_gps_coordinates": "Selected GPS Coordinates", "selected_gps_coordinates": "Selected GPS Coordinates",
"selected_people_to_merge": "Selected people to merge",
"send_message": "Send message", "send_message": "Send message",
"send_welcome_email": "Send welcome email", "send_welcome_email": "Send welcome email",
"server_endpoint": "Server Endpoint", "server_endpoint": "Server Endpoint",
@ -1970,6 +1971,7 @@
"show_album_options": "Show album options", "show_album_options": "Show album options",
"show_albums": "Show albums", "show_albums": "Show albums",
"show_all_people": "Show all people", "show_all_people": "Show all people",
"show_all_selected_people": "Show all selected people",
"show_and_hide_people": "Show & hide people", "show_and_hide_people": "Show & hide people",
"show_file_location": "Show file location", "show_file_location": "Show file location",
"show_gallery": "Show gallery", "show_gallery": "Show gallery",

@ -5,7 +5,7 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk'; import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk';
import { Button, Icon, IconButton, modalManager, toastManager } from '@immich/ui'; 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 { onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
@ -14,6 +14,7 @@
import ControlAppBar from '../shared-components/control-app-bar.svelte'; import ControlAppBar from '../shared-components/control-app-bar.svelte';
import FaceThumbnail from './face-thumbnail.svelte'; import FaceThumbnail from './face-thumbnail.svelte';
import PeopleList from './people-list.svelte'; import PeopleList from './people-list.svelte';
import PeopleViewModal from "$lib/modals/PeopleViewModal.svelte";
interface Props { interface Props {
person: PersonResponseDto; person: PersonResponseDto;
@ -49,11 +50,6 @@
return; return;
} }
if (selectedPeople.length >= 5) {
toastManager.warning($t('merge_people_limit'));
return;
}
selectedPeople = [selected, ...selectedPeople]; selectedPeople = [selected, ...selectedPeople];
if (selectedPeople.length === 1 && !person.name && selected.name) { if (selectedPeople.length === 1 && !person.name && selected.name) {
@ -80,6 +76,15 @@
handleError(error, $t('cannot_merge_people')); 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
});
};
</script> </script>
<svelte:window bind:innerHeight={screenHeight} /> <svelte:window bind:innerHeight={screenHeight} />
@ -109,7 +114,7 @@
<p class="mb-4 text-center uppercase dark:text-white">{$t('choose_matching_people_to_merge')}</p> <p class="mb-4 text-center uppercase dark:text-white">{$t('choose_matching_people_to_merge')}</p>
<div class="grid grid-flow-col-dense place-content-center place-items-center gap-4"> <div class="grid grid-flow-col-dense place-content-center place-items-center gap-4">
{#each selectedPeople as person (person.id)} {#each selectedPeople.slice(0, 5) as person, _ (person.id)}
<div animate:flip={{ duration: 250, easing: quintOut }}> <div animate:flip={{ duration: 250, easing: quintOut }}>
<FaceThumbnail border circle {person} selectable thumbnailSize={120} onClick={() => onSelect(person)} /> <FaceThumbnail border circle {person} selectable thumbnailSize={120} onClick={() => onSelect(person)} />
</div> </div>
@ -119,6 +124,18 @@
<div class="relative h-full"> <div class="relative h-full">
<div class="flex flex-col h-full justify-between"> <div class="flex flex-col h-full justify-between">
<div class="flex h-full items-center justify-center"> <div class="flex h-full items-center justify-center">
{#if selectedPeople.length > 5}
<div class="absolute top-2">
<IconButton
shape="round"
color="secondary"
aria-label={$t('show_all_selected_people')}
icon={mdiPlus}
size="medium"
onclick={showPeopleViewModal}
/>
</div>
{/if}
<Icon icon={mdiCallMerge} size="48" class="rotate-90 dark:text-white" /> <Icon icon={mdiCallMerge} size="48" class="rotate-90 dark:text-white" />
</div> </div>
{#if selectedPeople.length === 1} {#if selectedPeople.length === 1}

@ -16,7 +16,7 @@
let { screenHeight, people, peopleToNotShow, onSelect, handleSearch }: Props = $props(); let { screenHeight, people, peopleToNotShow, onSelect, handleSearch }: Props = $props();
let searchedPeopleLocal: PersonResponseDto[] = $state([]); let searchedPeopleLocal: PersonResponseDto[] = $state([]);
let sortBySimilarirty = $state(false); let sortBySimilarity = $state(false);
let name = $state(''); let name = $state('');
const showPeople = $derived( const showPeople = $derived(
@ -27,19 +27,19 @@
</script> </script>
<div class="w-40 sm:w-48 md:w-full h-14 flex gap-4 place-items-center"> <div class="w-40 sm:w-48 md:w-full h-14 flex gap-4 place-items-center">
<div class="md:w-96">
<SearchPeople type="searchBar" placeholder={$t('search_people')} bind:searchName={name} bind:searchedPeopleLocal />
</div>
{#if handleSearch} {#if handleSearch}
<div class="md:w-96">
<SearchPeople type="searchBar" placeholder={$t('search_people')} bind:searchName={name} bind:searchedPeopleLocal />
</div>
<IconButton <IconButton
shape="round" shape="round"
color="secondary" color="secondary"
variant="ghost" variant="ghost"
icon={mdiSwapVertical} icon={mdiSwapVertical}
onclick={() => { onclick={() => {
sortBySimilarirty = !sortBySimilarirty; sortBySimilarity = !sortBySimilarity;
handleSearch(sortBySimilarirty); handleSearch(sortBySimilarity);
}} }}
aria-label={$t('sort_people_by_similarity')} aria-label={$t('sort_people_by_similarity')}
/> />

@ -0,0 +1,31 @@
<script lang="ts">
import { Modal, ModalBody } from "@immich/ui";
import PeopleList from "$lib/components/faces-page/people-list.svelte";
import { t } from 'svelte-i18n';
import type { PersonResponseDto } from '@immich/sdk';
interface Props {
people: PersonResponseDto[];
peopleToNotShow: PersonResponseDto[];
screenHeight: number;
onClose: (people: PersonResponseDto[]) => void;
}
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;
}
};
</script>
<Modal title={$t('selected_people_to_merge')} size="full" onClose={() => onClose(people)}>
<ModalBody>
<p class="mb-4 text-center dark:text-white">{$t('choose_people_to_unselect')}</p>
<PeopleList {people} {peopleToNotShow} {screenHeight} {onSelect} />
</ModalBody>
</Modal>