merge-face-selector.svelte 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. <script lang="ts">
  2. import { createEventDispatcher, onMount } from 'svelte';
  3. import { api, type PersonResponseDto } from '@api';
  4. import FaceThumbnail from './face-thumbnail.svelte';
  5. import { quintOut } from 'svelte/easing';
  6. import { fly } from 'svelte/transition';
  7. import ControlAppBar from '../shared-components/control-app-bar.svelte';
  8. import Button from '../elements/buttons/button.svelte';
  9. import Merge from 'svelte-material-icons/Merge.svelte';
  10. import CallMerge from 'svelte-material-icons/CallMerge.svelte';
  11. import { flip } from 'svelte/animate';
  12. import { NotificationType, notificationController } from '../shared-components/notification/notification';
  13. import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
  14. import { handleError } from '$lib/utils/handle-error';
  15. import { invalidateAll } from '$app/navigation';
  16. export let person: PersonResponseDto;
  17. let people: PersonResponseDto[] = [];
  18. let selectedPeople: PersonResponseDto[] = [];
  19. let screenHeight: number;
  20. let isShowConfirmation = false;
  21. let dispatch = createEventDispatcher();
  22. $: hasSelection = selectedPeople.length > 0;
  23. $: unselectedPeople = people.filter((source) => !selectedPeople.includes(source) && source.id !== person.id);
  24. onMount(async () => {
  25. const { data } = await api.personApi.getAllPeople({ withHidden: false });
  26. people = data.people;
  27. });
  28. const onClose = () => {
  29. dispatch('go-back');
  30. };
  31. const onSelect = (selected: PersonResponseDto) => {
  32. if (selectedPeople.includes(selected)) {
  33. selectedPeople = selectedPeople.filter((person) => person.id !== selected.id);
  34. return;
  35. }
  36. if (selectedPeople.length >= 5) {
  37. notificationController.show({
  38. message: 'You can only merge up to 5 faces at a time',
  39. type: NotificationType.Info,
  40. });
  41. return;
  42. }
  43. selectedPeople = [selected, ...selectedPeople];
  44. };
  45. const handleMerge = async () => {
  46. try {
  47. const { data: results } = await api.personApi.mergePerson({
  48. id: person.id,
  49. mergePersonDto: { ids: selectedPeople.map(({ id }) => id) },
  50. });
  51. const count = results.filter(({ success }) => success).length;
  52. notificationController.show({
  53. message: `Merged ${count} ${count === 1 ? 'person' : 'people'}`,
  54. type: NotificationType.Info,
  55. });
  56. await invalidateAll();
  57. onClose();
  58. } catch (error) {
  59. handleError(error, 'Cannot merge faces');
  60. } finally {
  61. isShowConfirmation = false;
  62. }
  63. };
  64. </script>
  65. <svelte:window bind:innerHeight={screenHeight} />
  66. <section
  67. transition:fly={{ y: 500, duration: 100, easing: quintOut }}
  68. class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
  69. >
  70. <ControlAppBar on:close-button-click={onClose}>
  71. <svelte:fragment slot="leading">
  72. {#if hasSelection}
  73. Selected {selectedPeople.length}
  74. {:else}
  75. Merge faces
  76. {/if}
  77. <div />
  78. </svelte:fragment>
  79. <svelte:fragment slot="trailing">
  80. <Button
  81. size={'sm'}
  82. disabled={!hasSelection}
  83. on:click={() => {
  84. isShowConfirmation = true;
  85. }}
  86. >
  87. <Merge size={18} />
  88. <span class="ml-2"> Merge</span></Button
  89. >
  90. </svelte:fragment>
  91. </ControlAppBar>
  92. <section class="bg-immich-bg px-[70px] pt-[100px] dark:bg-immich-dark-bg">
  93. <section id="merge-face-selector relative">
  94. <div class="mb-10 h-[200px] place-content-center place-items-center">
  95. <p class="mb-4 text-center uppercase dark:text-white">Choose matching faces to merge</p>
  96. <div class="grid grid-flow-col-dense place-content-center place-items-center gap-4">
  97. {#each selectedPeople as person (person.id)}
  98. <div animate:flip={{ duration: 250, easing: quintOut }}>
  99. <FaceThumbnail border circle {person} selectable thumbnailSize={120} on:click={() => onSelect(person)} />
  100. </div>
  101. {/each}
  102. {#if hasSelection}
  103. <span><CallMerge size={48} class="rotate-90 dark:text-white" /> </span>
  104. {/if}
  105. <FaceThumbnail {person} border circle selectable={false} thumbnailSize={180} />
  106. </div>
  107. </div>
  108. <div
  109. class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray"
  110. style:max-height={screenHeight - 200 - 200 + 'px'}
  111. >
  112. <div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10">
  113. {#each unselectedPeople as person (person.id)}
  114. <FaceThumbnail {person} on:click={() => onSelect(person)} circle border selectable />
  115. {/each}
  116. </div>
  117. </div>
  118. </section>
  119. {#if isShowConfirmation}
  120. <ConfirmDialogue
  121. title="Merge faces"
  122. confirmText="Merge"
  123. on:confirm={handleMerge}
  124. on:cancel={() => (isShowConfirmation = false)}
  125. >
  126. <svelte:fragment slot="prompt">
  127. <p>Are you sure you want merge these faces? <br />This action is <strong>irreversible</strong>.</p>
  128. </svelte:fragment>
  129. </ConfirmDialogue>
  130. {/if}
  131. </section>
  132. </section>