mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	refactor(web): show & hide people (#10933)
This commit is contained in:
		
							parent
							
								
									cb40db9555
								
							
						
					
					
						commit
						e9683b326a
					
				@ -19,7 +19,7 @@
 | 
				
			|||||||
  export let hidden = false;
 | 
					  export let hidden = false;
 | 
				
			||||||
  export let border = false;
 | 
					  export let border = false;
 | 
				
			||||||
  export let preload = true;
 | 
					  export let preload = true;
 | 
				
			||||||
  export let eyeColor: 'black' | 'white' = 'white';
 | 
					  export let hiddenIconClass = 'text-white';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let complete = false;
 | 
					  let complete = false;
 | 
				
			||||||
  let img: HTMLImageElement;
 | 
					  let img: HTMLImageElement;
 | 
				
			||||||
@ -54,7 +54,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{#if hidden}
 | 
					{#if hidden}
 | 
				
			||||||
  <div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
 | 
					  <div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
 | 
				
			||||||
    <Icon {title} path={mdiEyeOffOutline} size="2em" class="text-{eyeColor}" />
 | 
					    <Icon {title} path={mdiEyeOffOutline} size="2em" class={hiddenIconClass} />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
{/if}
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,108 @@
 | 
				
			|||||||
 | 
					import { sdkMock } from '$lib/__mocks__/sdk.mock';
 | 
				
			||||||
 | 
					import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte';
 | 
				
			||||||
 | 
					import type { PersonResponseDto } from '@immich/sdk';
 | 
				
			||||||
 | 
					import { personFactory } from '@test-data/factories/person-factory';
 | 
				
			||||||
 | 
					import { render } from '@testing-library/svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('ManagePeopleVisibility Component', () => {
 | 
				
			||||||
 | 
					  let personVisible: PersonResponseDto;
 | 
				
			||||||
 | 
					  let personHidden: PersonResponseDto;
 | 
				
			||||||
 | 
					  let personWithoutName: PersonResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeAll(() => {
 | 
				
			||||||
 | 
					    // Prevents errors from `img.decode()` in ImageThumbnail
 | 
				
			||||||
 | 
					    Object.defineProperty(HTMLImageElement.prototype, 'decode', {
 | 
				
			||||||
 | 
					      value: vi.fn(),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    personVisible = personFactory.build({ isHidden: false });
 | 
				
			||||||
 | 
					    personHidden = personFactory.build({ isHidden: true });
 | 
				
			||||||
 | 
					    personWithoutName = personFactory.build({ isHidden: false, name: undefined });
 | 
				
			||||||
 | 
					    sdkMock.updatePeople.mockResolvedValue([]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterEach(() => {
 | 
				
			||||||
 | 
					    vi.resetAllMocks();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('does not update people when no changes are made', () => {
 | 
				
			||||||
 | 
					    const { getByText } = render(ManagePeopleVisibility, {
 | 
				
			||||||
 | 
					      props: {
 | 
				
			||||||
 | 
					        people: [personVisible, personHidden, personWithoutName],
 | 
				
			||||||
 | 
					        onClose: vi.fn(),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const saveButton = getByText('done');
 | 
				
			||||||
 | 
					    saveButton.click();
 | 
				
			||||||
 | 
					    expect(sdkMock.updatePeople).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('hides unnamed people on first button press', () => {
 | 
				
			||||||
 | 
					    const { getByText, getByTitle } = render(ManagePeopleVisibility, {
 | 
				
			||||||
 | 
					      props: {
 | 
				
			||||||
 | 
					        people: [personVisible, personHidden, personWithoutName],
 | 
				
			||||||
 | 
					        onClose: vi.fn(),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const toggleButton = getByTitle('toggle_visibility');
 | 
				
			||||||
 | 
					    toggleButton.click();
 | 
				
			||||||
 | 
					    const saveButton = getByText('done');
 | 
				
			||||||
 | 
					    saveButton.click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(sdkMock.updatePeople).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					      peopleUpdateDto: {
 | 
				
			||||||
 | 
					        people: [{ id: personWithoutName.id, isHidden: true }],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('hides all people on second button press', () => {
 | 
				
			||||||
 | 
					    const { getByText, getByTitle } = render(ManagePeopleVisibility, {
 | 
				
			||||||
 | 
					      props: {
 | 
				
			||||||
 | 
					        people: [personVisible, personHidden, personWithoutName],
 | 
				
			||||||
 | 
					        onClose: vi.fn(),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const toggleButton = getByTitle('toggle_visibility');
 | 
				
			||||||
 | 
					    toggleButton.click();
 | 
				
			||||||
 | 
					    toggleButton.click();
 | 
				
			||||||
 | 
					    const saveButton = getByText('done');
 | 
				
			||||||
 | 
					    saveButton.click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(sdkMock.updatePeople).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					      peopleUpdateDto: {
 | 
				
			||||||
 | 
					        people: expect.arrayContaining([
 | 
				
			||||||
 | 
					          { id: personVisible.id, isHidden: true },
 | 
				
			||||||
 | 
					          { id: personWithoutName.id, isHidden: true },
 | 
				
			||||||
 | 
					        ]),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('shows all people on third button press', () => {
 | 
				
			||||||
 | 
					    const { getByText, getByTitle } = render(ManagePeopleVisibility, {
 | 
				
			||||||
 | 
					      props: {
 | 
				
			||||||
 | 
					        people: [personVisible, personHidden, personWithoutName],
 | 
				
			||||||
 | 
					        onClose: vi.fn(),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const toggleButton = getByTitle('toggle_visibility');
 | 
				
			||||||
 | 
					    toggleButton.click();
 | 
				
			||||||
 | 
					    toggleButton.click();
 | 
				
			||||||
 | 
					    toggleButton.click();
 | 
				
			||||||
 | 
					    const saveButton = getByText('done');
 | 
				
			||||||
 | 
					    saveButton.click();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(sdkMock.updatePeople).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					      peopleUpdateDto: {
 | 
				
			||||||
 | 
					        people: [{ id: personHidden.id, isHidden: false }],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,162 @@
 | 
				
			|||||||
 | 
					<script lang="ts" context="module">
 | 
				
			||||||
 | 
					  const enum ToggleVisibility {
 | 
				
			||||||
 | 
					    HIDE_ALL = 'hide-all',
 | 
				
			||||||
 | 
					    HIDE_UNNANEMD = 'hide-unnamed',
 | 
				
			||||||
 | 
					    SHOW_ALL = 'show-all',
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					  import { shortcut } from '$lib/actions/shortcut';
 | 
				
			||||||
 | 
					  import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
 | 
				
			||||||
 | 
					  import Button from '$lib/components/elements/buttons/button.svelte';
 | 
				
			||||||
 | 
					  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
 | 
				
			||||||
 | 
					  import {
 | 
				
			||||||
 | 
					    notificationController,
 | 
				
			||||||
 | 
					    NotificationType,
 | 
				
			||||||
 | 
					  } from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
 | 
					  import { locale } from '$lib/stores/preferences.store';
 | 
				
			||||||
 | 
					  import { getPeopleThumbnailUrl } from '$lib/utils';
 | 
				
			||||||
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
 | 
					  import { updatePeople, type PersonResponseDto } from '@immich/sdk';
 | 
				
			||||||
 | 
					  import { mdiClose, mdiEye, mdiEyeOff, mdiEyeSettings, mdiRestart } from '@mdi/js';
 | 
				
			||||||
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
 | 
					  import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  export let people: PersonResponseDto[];
 | 
				
			||||||
 | 
					  export let onClose: () => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let toggleVisibility = ToggleVisibility.SHOW_ALL;
 | 
				
			||||||
 | 
					  let showLoadingSpinner = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $: personIsHidden = getPersonIsHidden(people);
 | 
				
			||||||
 | 
					  $: toggleIcon = toggleIconOptions[toggleVisibility];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getPersonIsHidden = (people: PersonResponseDto[]) => {
 | 
				
			||||||
 | 
					    const personIsHidden: Record<string, boolean> = {};
 | 
				
			||||||
 | 
					    for (const person of people) {
 | 
				
			||||||
 | 
					      personIsHidden[person.id] = person.isHidden;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return personIsHidden;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const toggleIconOptions: Record<ToggleVisibility, string> = {
 | 
				
			||||||
 | 
					    [ToggleVisibility.HIDE_ALL]: mdiEyeOff,
 | 
				
			||||||
 | 
					    [ToggleVisibility.HIDE_UNNANEMD]: mdiEyeSettings,
 | 
				
			||||||
 | 
					    [ToggleVisibility.SHOW_ALL]: mdiEye,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getNextVisibility = (toggleVisibility: ToggleVisibility) => {
 | 
				
			||||||
 | 
					    if (toggleVisibility === ToggleVisibility.SHOW_ALL) {
 | 
				
			||||||
 | 
					      return ToggleVisibility.HIDE_UNNANEMD;
 | 
				
			||||||
 | 
					    } else if (toggleVisibility === ToggleVisibility.HIDE_UNNANEMD) {
 | 
				
			||||||
 | 
					      return ToggleVisibility.HIDE_ALL;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return ToggleVisibility.SHOW_ALL;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleToggleVisibility = () => {
 | 
				
			||||||
 | 
					    toggleVisibility = getNextVisibility(toggleVisibility);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const person of people) {
 | 
				
			||||||
 | 
					      if (toggleVisibility === ToggleVisibility.HIDE_ALL) {
 | 
				
			||||||
 | 
					        personIsHidden[person.id] = true;
 | 
				
			||||||
 | 
					      } else if (toggleVisibility === ToggleVisibility.SHOW_ALL) {
 | 
				
			||||||
 | 
					        personIsHidden[person.id] = false;
 | 
				
			||||||
 | 
					      } else if (toggleVisibility === ToggleVisibility.HIDE_UNNANEMD && !person.name) {
 | 
				
			||||||
 | 
					        personIsHidden[person.id] = true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleResetVisibility = () => (personIsHidden = getPersonIsHidden(people));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSaveVisibility = async () => {
 | 
				
			||||||
 | 
					    showLoadingSpinner = true;
 | 
				
			||||||
 | 
					    const changed = people
 | 
				
			||||||
 | 
					      .filter((person) => person.isHidden !== personIsHidden[person.id])
 | 
				
			||||||
 | 
					      .map((person) => ({ id: person.id, isHidden: personIsHidden[person.id] }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      if (changed.length > 0) {
 | 
				
			||||||
 | 
					        const results = await updatePeople({ peopleUpdateDto: { people: changed } });
 | 
				
			||||||
 | 
					        const successCount = results.filter(({ success }) => success).length;
 | 
				
			||||||
 | 
					        const failCount = results.length - successCount;
 | 
				
			||||||
 | 
					        if (failCount > 0) {
 | 
				
			||||||
 | 
					          notificationController.show({
 | 
				
			||||||
 | 
					            type: NotificationType.Error,
 | 
				
			||||||
 | 
					            message: $t('errors.unable_to_change_visibility', { values: { count: failCount } }),
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        notificationController.show({
 | 
				
			||||||
 | 
					          type: NotificationType.Info,
 | 
				
			||||||
 | 
					          message: $t('visibility_changed', { values: { count: successCount } }),
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for (const person of people) {
 | 
				
			||||||
 | 
					        person.isHidden = personIsHidden[person.id];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      people = people;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      onClose();
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      handleError(error, $t('errors.unable_to_change_visibility', { values: { count: changed.length } }));
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      showLoadingSpinner = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<svelte:window use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div
 | 
				
			||||||
 | 
					  class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					  <div class="flex items-center">
 | 
				
			||||||
 | 
					    <CircleIconButton title={$t('close')} icon={mdiClose} on:click={onClose} />
 | 
				
			||||||
 | 
					    <div class="flex gap-2 items-center">
 | 
				
			||||||
 | 
					      <p class="ml-2">{$t('show_and_hide_people')}</p>
 | 
				
			||||||
 | 
					      <p class="text-sm text-gray-400 dark:text-gray-600">({people.length.toLocaleString($locale)})</p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <div class="flex items-center justify-end">
 | 
				
			||||||
 | 
					    <div class="flex items-center md:mr-4">
 | 
				
			||||||
 | 
					      <CircleIconButton title={$t('reset_people_visibility')} icon={mdiRestart} on:click={handleResetVisibility} />
 | 
				
			||||||
 | 
					      <CircleIconButton title={$t('toggle_visibility')} icon={toggleIcon} on:click={handleToggleVisibility} />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    {#if !showLoadingSpinner}
 | 
				
			||||||
 | 
					      <Button on:click={handleSaveVisibility} size="sm" rounded="lg">{$t('done')}</Button>
 | 
				
			||||||
 | 
					    {:else}
 | 
				
			||||||
 | 
					      <LoadingSpinner />
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="flex flex-wrap gap-1 bg-immich-bg p-2 pb-8 dark:bg-immich-dark-bg md:px-8 mt-16">
 | 
				
			||||||
 | 
					  <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">
 | 
				
			||||||
 | 
					    {#each people as person, index (person.id)}
 | 
				
			||||||
 | 
					      <button
 | 
				
			||||||
 | 
					        type="button"
 | 
				
			||||||
 | 
					        class="group relative"
 | 
				
			||||||
 | 
					        on:click={() => (personIsHidden[person.id] = !personIsHidden[person.id])}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <ImageThumbnail
 | 
				
			||||||
 | 
					          preload={index < 20}
 | 
				
			||||||
 | 
					          hidden={personIsHidden[person.id]}
 | 
				
			||||||
 | 
					          shadow
 | 
				
			||||||
 | 
					          url={getPeopleThumbnailUrl(person)}
 | 
				
			||||||
 | 
					          altText={person.name}
 | 
				
			||||||
 | 
					          widthStyle="100%"
 | 
				
			||||||
 | 
					          hiddenIconClass="text-white group-hover:text-black transition-colors"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        {#if person.name}
 | 
				
			||||||
 | 
					          <span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
 | 
				
			||||||
 | 
					            {person.name}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        {/if}
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    {/each}
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@ -1,81 +0,0 @@
 | 
				
			|||||||
<script lang="ts" context="module">
 | 
					 | 
				
			||||||
  export enum ToggleVisibilty {
 | 
					 | 
				
			||||||
    HIDE_ALL = 'hide-all',
 | 
					 | 
				
			||||||
    HIDE_UNNANEMD = 'hide-unnamed',
 | 
					 | 
				
			||||||
    VIEW_ALL = 'view-all',
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
  import { fly } from 'svelte/transition';
 | 
					 | 
				
			||||||
  import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
 | 
					 | 
				
			||||||
  import { quintOut } from 'svelte/easing';
 | 
					 | 
				
			||||||
  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
 | 
					 | 
				
			||||||
  import { mdiClose, mdiEye, mdiEyeOff, mdiEyeSettings, mdiRestart } from '@mdi/js';
 | 
					 | 
				
			||||||
  import { locale } from '$lib/stores/preferences.store';
 | 
					 | 
				
			||||||
  import Button from '$lib/components/elements/buttons/button.svelte';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  export let showLoadingSpinner: boolean;
 | 
					 | 
				
			||||||
  export let toggleVisibility: ToggleVisibilty = ToggleVisibilty.VIEW_ALL;
 | 
					 | 
				
			||||||
  export let screenHeight: number;
 | 
					 | 
				
			||||||
  export let countTotalPeople: number;
 | 
					 | 
				
			||||||
  export let onClose: () => void;
 | 
					 | 
				
			||||||
  export let onReset: () => void;
 | 
					 | 
				
			||||||
  export let onChange: (toggleVisibility: ToggleVisibilty) => void;
 | 
					 | 
				
			||||||
  export let onDone: () => void;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const getNextVisibility = (toggleVisibility: ToggleVisibilty) => {
 | 
					 | 
				
			||||||
    if (toggleVisibility === ToggleVisibilty.VIEW_ALL) {
 | 
					 | 
				
			||||||
      return ToggleVisibilty.HIDE_UNNANEMD;
 | 
					 | 
				
			||||||
    } else if (toggleVisibility === ToggleVisibilty.HIDE_UNNANEMD) {
 | 
					 | 
				
			||||||
      return ToggleVisibilty.HIDE_ALL;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return ToggleVisibilty.VIEW_ALL;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const toggleIconOptions: Record<ToggleVisibilty, string> = {
 | 
					 | 
				
			||||||
    [ToggleVisibilty.HIDE_ALL]: mdiEyeOff,
 | 
					 | 
				
			||||||
    [ToggleVisibilty.HIDE_UNNANEMD]: mdiEyeSettings,
 | 
					 | 
				
			||||||
    [ToggleVisibilty.VIEW_ALL]: mdiEye,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  $: toggleIcon = toggleIconOptions[toggleVisibility];
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<section
 | 
					 | 
				
			||||||
  transition:fly={{ y: screenHeight, duration: 150, easing: quintOut, opacity: 1 }}
 | 
					 | 
				
			||||||
  class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
 | 
					 | 
				
			||||||
>
 | 
					 | 
				
			||||||
  <div
 | 
					 | 
				
			||||||
    class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <div class="flex items-center">
 | 
					 | 
				
			||||||
      <CircleIconButton title={$t('close')} icon={mdiClose} on:click={onClose} />
 | 
					 | 
				
			||||||
      <div class="flex gap-2 items-center">
 | 
					 | 
				
			||||||
        <p class="ml-2">{$t('show_and_hide_people')}</p>
 | 
					 | 
				
			||||||
        <p class="text-sm text-gray-400 dark:text-gray-600">({countTotalPeople.toLocaleString($locale)})</p>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <div class="flex items-center justify-end">
 | 
					 | 
				
			||||||
      <div class="flex items-center md:mr-8">
 | 
					 | 
				
			||||||
        <CircleIconButton title={$t('reset_people_visibility')} icon={mdiRestart} on:click={onReset} />
 | 
					 | 
				
			||||||
        <CircleIconButton
 | 
					 | 
				
			||||||
          title={$t('toggle_visibility')}
 | 
					 | 
				
			||||||
          icon={toggleIcon}
 | 
					 | 
				
			||||||
          on:click={() => onChange(getNextVisibility(toggleVisibility))}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      {#if !showLoadingSpinner}
 | 
					 | 
				
			||||||
        <Button on:click={onDone} size="sm" rounded="lg">{$t('done')}</Button>
 | 
					 | 
				
			||||||
      {:else}
 | 
					 | 
				
			||||||
        <LoadingSpinner />
 | 
					 | 
				
			||||||
      {/if}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div class="flex flex-wrap gap-1 bg-immich-bg p-2 pb-8 dark:bg-immich-dark-bg md:px-8 mt-16">
 | 
					 | 
				
			||||||
    <slot />
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</section>
 | 
					 | 
				
			||||||
@ -1,13 +1,14 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { goto } from '$app/navigation';
 | 
					  import { goto } from '$app/navigation';
 | 
				
			||||||
  import { page } from '$app/stores';
 | 
					  import { page } from '$app/stores';
 | 
				
			||||||
  import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
 | 
					 | 
				
			||||||
  import Button from '$lib/components/elements/buttons/button.svelte';
 | 
					  import Button from '$lib/components/elements/buttons/button.svelte';
 | 
				
			||||||
 | 
					  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
 | 
				
			||||||
  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 MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.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 SearchPeople from '$lib/components/faces-page/people-search.svelte';
 | 
				
			||||||
  import SetBirthDateModal from '$lib/components/faces-page/set-birth-date-modal.svelte';
 | 
					  import SetBirthDateModal from '$lib/components/faces-page/set-birth-date-modal.svelte';
 | 
				
			||||||
  import ShowHide, { ToggleVisibilty } from '$lib/components/faces-page/show-hide.svelte';
 | 
					 | 
				
			||||||
  import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
 | 
					  import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
 | 
				
			||||||
  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
 | 
					  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
 | 
				
			||||||
  import {
 | 
					  import {
 | 
				
			||||||
@ -15,43 +16,28 @@
 | 
				
			|||||||
    NotificationType,
 | 
					    NotificationType,
 | 
				
			||||||
  } from '$lib/components/shared-components/notification/notification';
 | 
					  } from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
  import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants';
 | 
					  import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants';
 | 
				
			||||||
  import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
 | 
					  import { locale } from '$lib/stores/preferences.store';
 | 
				
			||||||
 | 
					  import { websocketEvents } from '$lib/stores/websocket';
 | 
				
			||||||
 | 
					  import { handlePromiseError } from '$lib/utils';
 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
  import { shortcut } from '$lib/actions/shortcut';
 | 
					  import { clearQueryParam } from '$lib/utils/navigation';
 | 
				
			||||||
  import {
 | 
					  import { getPerson, mergePerson, searchPerson, updatePerson, type PersonResponseDto } from '@immich/sdk';
 | 
				
			||||||
    getPerson,
 | 
					 | 
				
			||||||
    mergePerson,
 | 
					 | 
				
			||||||
    searchPerson,
 | 
					 | 
				
			||||||
    updatePeople,
 | 
					 | 
				
			||||||
    updatePerson,
 | 
					 | 
				
			||||||
    type PeopleUpdateItem,
 | 
					 | 
				
			||||||
    type PersonResponseDto,
 | 
					 | 
				
			||||||
  } from '@immich/sdk';
 | 
					 | 
				
			||||||
  import { mdiAccountOff, mdiEyeOutline } from '@mdi/js';
 | 
					  import { mdiAccountOff, mdiEyeOutline } from '@mdi/js';
 | 
				
			||||||
  import { onMount } from 'svelte';
 | 
					  import { onMount } from 'svelte';
 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					 | 
				
			||||||
  import { locale } from '$lib/stores/preferences.store';
 | 
					 | 
				
			||||||
  import { clearQueryParam } from '$lib/utils/navigation';
 | 
					 | 
				
			||||||
  import SearchPeople from '$lib/components/faces-page/people-search.svelte';
 | 
					 | 
				
			||||||
  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import { websocketEvents } from '$lib/stores/websocket';
 | 
					  import { quintOut } from 'svelte/easing';
 | 
				
			||||||
 | 
					  import { fly } from 'svelte/transition';
 | 
				
			||||||
 | 
					  import type { PageData } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let data: PageData;
 | 
					  export let data: PageData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let people = data.people.people;
 | 
					  $: people = data.people.people;
 | 
				
			||||||
  let countTotalPeople = data.people.total;
 | 
					  $: visiblePeople = people.filter((people) => !people.isHidden);
 | 
				
			||||||
  let countHiddenPeople = data.people.hidden;
 | 
					  $: countVisiblePeople = searchName ? searchedPeopleLocal.length : visiblePeople.length;
 | 
				
			||||||
 | 
					  $: showPeople = searchName ? searchedPeopleLocal : visiblePeople;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let selectHidden = false;
 | 
					  let selectHidden = false;
 | 
				
			||||||
  let initialHiddenValues: Record<string, boolean> = {};
 | 
					 | 
				
			||||||
  let eyeColorMap: Record<string, 'black' | 'white'> = {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let searchName = '';
 | 
					  let searchName = '';
 | 
				
			||||||
 | 
					 | 
				
			||||||
  let showLoadingSpinner = false;
 | 
					 | 
				
			||||||
  let toggleVisibility: ToggleVisibilty = ToggleVisibilty.VIEW_ALL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let showChangeNameModal = false;
 | 
					  let showChangeNameModal = false;
 | 
				
			||||||
  let showSetBirthDateModal = false;
 | 
					  let showSetBirthDateModal = false;
 | 
				
			||||||
  let showMergeModal = false;
 | 
					  let showMergeModal = false;
 | 
				
			||||||
@ -62,24 +48,9 @@
 | 
				
			|||||||
  let edittingPerson: PersonResponseDto | null = null;
 | 
					  let edittingPerson: PersonResponseDto | null = null;
 | 
				
			||||||
  let searchedPeopleLocal: PersonResponseDto[] = [];
 | 
					  let searchedPeopleLocal: PersonResponseDto[] = [];
 | 
				
			||||||
  let handleSearchPeople: (force?: boolean, name?: string) => Promise<void>;
 | 
					  let handleSearchPeople: (force?: boolean, name?: string) => Promise<void>;
 | 
				
			||||||
  let showPeople: PersonResponseDto[] = [];
 | 
					 | 
				
			||||||
  let countVisiblePeople: number;
 | 
					 | 
				
			||||||
  let changeNameInputEl: HTMLInputElement | null;
 | 
					  let changeNameInputEl: HTMLInputElement | null;
 | 
				
			||||||
  let innerHeight: number;
 | 
					  let innerHeight: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for (const person of people) {
 | 
					 | 
				
			||||||
    initialHiddenValues[person.id] = person.isHidden;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  $: {
 | 
					 | 
				
			||||||
    if (searchName) {
 | 
					 | 
				
			||||||
      showPeople = searchedPeopleLocal;
 | 
					 | 
				
			||||||
      countVisiblePeople = searchedPeopleLocal.length;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      showPeople = people.filter((person) => !person.isHidden);
 | 
					 | 
				
			||||||
      countVisiblePeople = countTotalPeople - countHiddenPeople;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onMount(() => {
 | 
					  onMount(() => {
 | 
				
			||||||
    const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE);
 | 
					    const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE);
 | 
				
			||||||
    if (getSearchedPeople) {
 | 
					    if (getSearchedPeople) {
 | 
				
			||||||
@ -87,11 +58,12 @@
 | 
				
			|||||||
      handlePromiseError(handleSearchPeople(true, searchName));
 | 
					      handlePromiseError(handleSearchPeople(true, searchName));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return websocketEvents.on('on_person_thumbnail', (personId: string) => {
 | 
					    return websocketEvents.on('on_person_thumbnail', (personId: string) => {
 | 
				
			||||||
      people.map((person) => {
 | 
					      for (const person of people) {
 | 
				
			||||||
        if (person.id === personId) {
 | 
					        if (person.id === personId) {
 | 
				
			||||||
          person.updatedAt = Date.now().toString();
 | 
					          person.updatedAt = new Date().toISOString();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      });
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // trigger reactivity
 | 
					      // trigger reactivity
 | 
				
			||||||
      people = people;
 | 
					      people = people;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -105,89 +77,6 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleCloseClick = () => {
 | 
					 | 
				
			||||||
    for (const person of people) {
 | 
					 | 
				
			||||||
      person.isHidden = initialHiddenValues[person.id];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // trigger reactivity
 | 
					 | 
				
			||||||
    people = people;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Reset variables used on the "Show & hide people"   modal
 | 
					 | 
				
			||||||
    showLoadingSpinner = false;
 | 
					 | 
				
			||||||
    selectHidden = false;
 | 
					 | 
				
			||||||
    toggleVisibility = ToggleVisibilty.VIEW_ALL;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleResetVisibility = () => {
 | 
					 | 
				
			||||||
    for (const person of people) {
 | 
					 | 
				
			||||||
      person.isHidden = initialHiddenValues[person.id];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // trigger reactivity
 | 
					 | 
				
			||||||
    people = people;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleToggleVisibility = (toggleVisibility: ToggleVisibilty) => {
 | 
					 | 
				
			||||||
    for (const person of people) {
 | 
					 | 
				
			||||||
      if (toggleVisibility == ToggleVisibilty.HIDE_ALL) {
 | 
					 | 
				
			||||||
        person.isHidden = true;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (toggleVisibility == ToggleVisibilty.VIEW_ALL) {
 | 
					 | 
				
			||||||
        person.isHidden = false;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (toggleVisibility == ToggleVisibilty.HIDE_UNNANEMD && !person.name) {
 | 
					 | 
				
			||||||
        person.isHidden = true;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // trigger reactivity
 | 
					 | 
				
			||||||
    people = people;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleDoneClick = async () => {
 | 
					 | 
				
			||||||
    showLoadingSpinner = true;
 | 
					 | 
				
			||||||
    let changed: PeopleUpdateItem[] = [];
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      // Check if the visibility for each person has been changed
 | 
					 | 
				
			||||||
      for (const person of people) {
 | 
					 | 
				
			||||||
        if (person.isHidden !== initialHiddenValues[person.id]) {
 | 
					 | 
				
			||||||
          changed.push({ id: person.id, isHidden: person.isHidden });
 | 
					 | 
				
			||||||
          if (person.isHidden) {
 | 
					 | 
				
			||||||
            countHiddenPeople++;
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            countHiddenPeople--;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          // Update the initial hidden values
 | 
					 | 
				
			||||||
          initialHiddenValues[person.id] = person.isHidden;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (changed.length > 0) {
 | 
					 | 
				
			||||||
        const results = await updatePeople({
 | 
					 | 
				
			||||||
          peopleUpdateDto: { people: changed },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        const count = results.filter(({ success }) => success).length;
 | 
					 | 
				
			||||||
        if (results.length - count > 0) {
 | 
					 | 
				
			||||||
          notificationController.show({
 | 
					 | 
				
			||||||
            type: NotificationType.Error,
 | 
					 | 
				
			||||||
            message: $t('errors.unable_to_change_visibility', { values: { count: results.length - count } }),
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        notificationController.show({
 | 
					 | 
				
			||||||
          type: NotificationType.Info,
 | 
					 | 
				
			||||||
          message: $t('visibility_changed', { values: { count: count } }),
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      handleError(error, $t('errors.unable_to_change_visibility', { values: { count: changed.length } }));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // Reset variables used on the "Show & hide people" modal
 | 
					 | 
				
			||||||
    showLoadingSpinner = false;
 | 
					 | 
				
			||||||
    selectHidden = false;
 | 
					 | 
				
			||||||
    toggleVisibility = ToggleVisibilty.VIEW_ALL;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => {
 | 
					  const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => {
 | 
				
			||||||
    const [personToMerge, personToBeMergedIn] = response;
 | 
					    const [personToMerge, personToBeMergedIn] = response;
 | 
				
			||||||
    showMergeModal = false;
 | 
					    showMergeModal = false;
 | 
				
			||||||
@ -205,10 +94,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      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 === personToBeMergedIn.id ? mergedPerson : person));
 | 
				
			||||||
      if (personToMerge.isHidden) {
 | 
					 | 
				
			||||||
        countHiddenPeople--;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      countTotalPeople--;
 | 
					 | 
				
			||||||
      notificationController.show({
 | 
					      notificationController.show({
 | 
				
			||||||
        message: $t('merge_people_successfully'),
 | 
					        message: $t('merge_people_successfully'),
 | 
				
			||||||
        type: NotificationType.Info,
 | 
					        type: NotificationType.Info,
 | 
				
			||||||
@ -273,12 +158,7 @@
 | 
				
			|||||||
        return person;
 | 
					        return person;
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (const person of people) {
 | 
					 | 
				
			||||||
        initialHiddenValues[person.id] = person.isHidden;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      showChangeNameModal = false;
 | 
					      showChangeNameModal = false;
 | 
				
			||||||
      countHiddenPeople++;
 | 
					 | 
				
			||||||
      notificationController.show({
 | 
					      notificationController.show({
 | 
				
			||||||
        message: $t('changed_visibility_successfully'),
 | 
					        message: $t('changed_visibility_successfully'),
 | 
				
			||||||
        type: NotificationType.Info,
 | 
					        type: NotificationType.Info,
 | 
				
			||||||
@ -391,7 +271,7 @@
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<svelte:window bind:innerHeight use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: handleCloseClick }} />
 | 
					<svelte:window bind:innerHeight />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#if showMergeModal}
 | 
					{#if showMergeModal}
 | 
				
			||||||
  <MergeSuggestionModal
 | 
					  <MergeSuggestionModal
 | 
				
			||||||
@ -409,7 +289,7 @@
 | 
				
			|||||||
  description={countVisiblePeople === 0 && !searchName ? undefined : `(${countVisiblePeople.toLocaleString($locale)})`}
 | 
					  description={countVisiblePeople === 0 && !searchName ? undefined : `(${countVisiblePeople.toLocaleString($locale)})`}
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
  <svelte:fragment slot="buttons">
 | 
					  <svelte:fragment slot="buttons">
 | 
				
			||||||
    {#if countTotalPeople > 0}
 | 
					    {#if people.length > 0}
 | 
				
			||||||
      <div class="flex gap-2 items-center justify-center">
 | 
					      <div class="flex gap-2 items-center justify-center">
 | 
				
			||||||
        <div class="hidden sm:block">
 | 
					        <div class="hidden sm:block">
 | 
				
			||||||
          <div class="w-40 lg:w-80 h-10">
 | 
					          <div class="w-40 lg:w-80 h-10">
 | 
				
			||||||
@ -494,42 +374,13 @@
 | 
				
			|||||||
    />
 | 
					    />
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
</UserPageLayout>
 | 
					</UserPageLayout>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#if selectHidden}
 | 
					{#if selectHidden}
 | 
				
			||||||
  <ShowHide
 | 
					  <!-- TODO: Add focus trap -->
 | 
				
			||||||
    onDone={handleDoneClick}
 | 
					  <section
 | 
				
			||||||
    onClose={handleCloseClick}
 | 
					    transition:fly={{ y: innerHeight, duration: 150, easing: quintOut, opacity: 0 }}
 | 
				
			||||||
    onReset={handleResetVisibility}
 | 
					    class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
 | 
				
			||||||
    onChange={handleToggleVisibility}
 | 
					 | 
				
			||||||
    bind:showLoadingSpinner
 | 
					 | 
				
			||||||
    bind:toggleVisibility
 | 
					 | 
				
			||||||
    {countTotalPeople}
 | 
					 | 
				
			||||||
    screenHeight={innerHeight}
 | 
					 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <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">
 | 
					    <ManagePeopleVisibility bind:people onClose={() => (selectHidden = false)} />
 | 
				
			||||||
      {#each people as person, index (person.id)}
 | 
					  </section>
 | 
				
			||||||
        <button
 | 
					 | 
				
			||||||
          type="button"
 | 
					 | 
				
			||||||
          class="relative"
 | 
					 | 
				
			||||||
          on:click={() => (person.isHidden = !person.isHidden)}
 | 
					 | 
				
			||||||
          on:mouseenter={() => (eyeColorMap[person.id] = 'black')}
 | 
					 | 
				
			||||||
          on:mouseleave={() => (eyeColorMap[person.id] = 'white')}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <ImageThumbnail
 | 
					 | 
				
			||||||
            preload={searchName !== '' || index < 20}
 | 
					 | 
				
			||||||
            bind:hidden={person.isHidden}
 | 
					 | 
				
			||||||
            shadow
 | 
					 | 
				
			||||||
            url={getPeopleThumbnailUrl(person)}
 | 
					 | 
				
			||||||
            altText={person.name}
 | 
					 | 
				
			||||||
            widthStyle="100%"
 | 
					 | 
				
			||||||
            bind:eyeColor={eyeColorMap[person.id]}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          {#if person.name}
 | 
					 | 
				
			||||||
            <span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
 | 
					 | 
				
			||||||
              {person.name}
 | 
					 | 
				
			||||||
            </span>
 | 
					 | 
				
			||||||
          {/if}
 | 
					 | 
				
			||||||
        </button>
 | 
					 | 
				
			||||||
      {/each}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </ShowHide>
 | 
					 | 
				
			||||||
{/if}
 | 
					{/if}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								web/src/test-data/factories/person-factory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								web/src/test-data/factories/person-factory.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { faker } from '@faker-js/faker';
 | 
				
			||||||
 | 
					import type { PersonResponseDto } from '@immich/sdk';
 | 
				
			||||||
 | 
					import { Sync } from 'factory.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const personFactory = Sync.makeFactory<PersonResponseDto>({
 | 
				
			||||||
 | 
					  birthDate: Sync.each(() => faker.date.past().toISOString()),
 | 
				
			||||||
 | 
					  id: Sync.each(() => faker.string.uuid()),
 | 
				
			||||||
 | 
					  isHidden: Sync.each(() => faker.datatype.boolean()),
 | 
				
			||||||
 | 
					  name: Sync.each(() => faker.person.fullName()),
 | 
				
			||||||
 | 
					  thumbnailPath: Sync.each(() => faker.system.filePath()),
 | 
				
			||||||
 | 
					  updatedAt: Sync.each(() => faker.date.recent().toISOString()),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user