mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat: people infinite scroll (#11326)
* feat: people infinite scroll * add infinite scroll to show & hide modal * update unit tests * show total people count instead of currently loaded * update personsearchdto
This commit is contained in:
parent
152421e288
commit
8e6bc13540
17 changed files with 276 additions and 67 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import { getIntersectionObserverMock } from '$lib/__mocks__/intersection-observer.mock';
|
||||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||
import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte';
|
||||
import type { PersonResponseDto } from '@immich/sdk';
|
||||
|
|
@ -18,6 +19,7 @@ describe('ManagePeopleVisibility Component', () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock());
|
||||
personVisible = personFactory.build({ isHidden: false });
|
||||
personHidden = personFactory.build({ isHidden: true });
|
||||
personWithoutName = personFactory.build({ isHidden: false, name: undefined });
|
||||
|
|
@ -32,7 +34,9 @@ describe('ManagePeopleVisibility Component', () => {
|
|||
const { getByText } = render(ManagePeopleVisibility, {
|
||||
props: {
|
||||
people: [personVisible, personHidden, personWithoutName],
|
||||
totalPeopleCount: 3,
|
||||
onClose: vi.fn(),
|
||||
loadNextPage: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -45,7 +49,9 @@ describe('ManagePeopleVisibility Component', () => {
|
|||
const { getByText, getByTitle } = render(ManagePeopleVisibility, {
|
||||
props: {
|
||||
people: [personVisible, personHidden, personWithoutName],
|
||||
totalPeopleCount: 3,
|
||||
onClose: vi.fn(),
|
||||
loadNextPage: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -63,7 +69,9 @@ describe('ManagePeopleVisibility Component', () => {
|
|||
const { getByText, getByTitle } = render(ManagePeopleVisibility, {
|
||||
props: {
|
||||
people: [personVisible, personHidden, personWithoutName],
|
||||
totalPeopleCount: 3,
|
||||
onClose: vi.fn(),
|
||||
loadNextPage: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -86,7 +94,9 @@ describe('ManagePeopleVisibility Component', () => {
|
|||
const { getByText, getByTitle } = render(ManagePeopleVisibility, {
|
||||
props: {
|
||||
people: [personVisible, personHidden, personWithoutName],
|
||||
totalPeopleCount: 3,
|
||||
onClose: vi.fn(),
|
||||
loadNextPage: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
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 PeopleInfiniteScroll from '$lib/components/faces-page/people-infinite-scroll.svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
|
|
@ -24,8 +25,10 @@
|
|||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
|
||||
export let people: PersonResponseDto[];
|
||||
export let onClose: () => void;
|
||||
export let totalPeopleCount: number;
|
||||
export let titleId: string | undefined = undefined;
|
||||
export let onClose: () => void;
|
||||
export let loadNextPage: () => void;
|
||||
|
||||
let toggleVisibility = ToggleVisibility.SHOW_ALL;
|
||||
let showLoadingSpinner = false;
|
||||
|
|
@ -121,7 +124,7 @@
|
|||
<CircleIconButton title={$t('close')} icon={mdiClose} on:click={onClose} />
|
||||
<div class="flex gap-2 items-center">
|
||||
<p id={titleId} 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>
|
||||
<p class="text-sm text-gray-400 dark:text-gray-600">({totalPeopleCount.toLocaleString($locale)})</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end">
|
||||
|
|
@ -138,31 +141,29 @@
|
|||
</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)}
|
||||
{@const hidden = personIsHidden[person.id]}
|
||||
<button
|
||||
type="button"
|
||||
class="group relative"
|
||||
on:click={() => (personIsHidden[person.id] = !hidden)}
|
||||
aria-pressed={hidden}
|
||||
aria-label={person.name ? $t('hide_named_person', { values: { name: person.name } }) : $t('hide_person')}
|
||||
>
|
||||
<ImageThumbnail
|
||||
preload={index < 20}
|
||||
{hidden}
|
||||
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>
|
||||
<PeopleInfiniteScroll {people} hasNextPage={true} {loadNextPage} let:person let:index>
|
||||
{@const hidden = personIsHidden[person.id]}
|
||||
<button
|
||||
type="button"
|
||||
class="group relative"
|
||||
on:click={() => (personIsHidden[person.id] = !hidden)}
|
||||
aria-pressed={hidden}
|
||||
aria-label={person.name ? $t('hide_named_person', { values: { name: person.name } }) : $t('hide_person')}
|
||||
>
|
||||
<ImageThumbnail
|
||||
preload={index < 20}
|
||||
{hidden}
|
||||
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>
|
||||
</PeopleInfiniteScroll>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
import type { PersonResponseDto } from '@immich/sdk';
|
||||
|
||||
export let people: PersonResponseDto[];
|
||||
export let hasNextPage: boolean | undefined = undefined;
|
||||
export let loadNextPage: () => void;
|
||||
|
||||
let lastPersonContainer: HTMLElement | undefined;
|
||||
|
||||
const intersectionObserver = new IntersectionObserver((entries) => {
|
||||
const entry = entries.find((entry) => entry.target === lastPersonContainer);
|
||||
if (entry?.isIntersecting) {
|
||||
loadNextPage();
|
||||
}
|
||||
});
|
||||
|
||||
$: if (lastPersonContainer) {
|
||||
intersectionObserver.disconnect();
|
||||
intersectionObserver.observe(lastPersonContainer);
|
||||
}
|
||||
</script>
|
||||
|
||||
<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)}
|
||||
{#if hasNextPage && index === people.length - 1}
|
||||
<div bind:this={lastPersonContainer}>
|
||||
<slot {person} {index} />
|
||||
</div>
|
||||
{:else}
|
||||
<slot {person} {index} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue