Files
immich/web/src/lib/components/faces-page/assign-face-side-panel.svelte
Min Idzelis 837b1e4929 feat(web): Scroll to asset in gridview; increase gridview perf; reduce memory; scrollbar ticks in fixed position (#10646)
* Squashed

* Change strategy - now pre-measure buckets offscreen, so don't need to worry about sub-bucket scroll preservation

* Reduce jank on scroll, delay DOM updates until after scroll

* css opt, log measure time

* Trickle out queue while scrolling, flush when stopped

* yay

* Cleanup cleanup...

* everybody...

* everywhere...

* Clean up cleanup!

* Everybody do their share

* CLEANUP!

* package-lock ?

* dynamic measure, todo

* Fix web test

* type lint

* fix e2e

* e2e test

* Better scrollbar

* Tuning, and more tunables

* Tunable tweaks, more tunables

* Scrollbar dots and viewport events

* lint

* Tweaked tunnables, use requestIdleCallback for garbage tasks, bug fixes

* New tunables, and don't update url by default

* Bug fixes

* Bug fix, with debug

* Fix flickr, fix graybox bug, reduced debug

* Refactor/cleanup

* Fix

* naming

* Final cleanup

* review comment

* Forgot to update this after naming change

* scrubber works, with debug

* cleanup

* Rename scrollbar to scrubber

* rename  to

* left over rename and change to previous album bar

* bugfix addassets, comments

* missing destroy(), cleanup

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-21 21:15:21 -05:00

130 lines
4.8 KiB
Svelte

<script lang="ts">
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
import { getPersonNameWithHiddenValue } from '$lib/utils/person';
import { getPeopleThumbnailUrl } from '$lib/utils';
import { AssetTypeEnum, type AssetFaceResponseDto, type PersonResponseDto } from '@immich/sdk';
import { mdiArrowLeftThin, mdiClose, mdiMagnify, mdiPlus } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import { linear } from 'svelte/easing';
import { fly } from 'svelte/transition';
import { photoViewer } from '$lib/stores/assets.store';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { zoomImageToBase64 } from '$lib/utils/people-utils';
import { t } from 'svelte-i18n';
export let allPeople: PersonResponseDto[];
export let editedFace: AssetFaceResponseDto;
export let assetId: string;
export let assetType: AssetTypeEnum;
// loading spinners
let isShowLoadingNewPerson = false;
let isShowLoadingSearch = false;
// search people
let searchedPeople: PersonResponseDto[] = [];
let searchFaces = false;
let searchName = '';
$: showPeople = searchName ? searchedPeople : allPeople.filter((person) => !person.isHidden);
const dispatch = createEventDispatcher<{
close: void;
createPerson: string | null;
reassign: PersonResponseDto;
}>();
const handleBackButton = () => {
dispatch('close');
};
const handleCreatePerson = async () => {
const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner);
const newFeaturePhoto = await zoomImageToBase64(editedFace, assetId, assetType, $photoViewer);
dispatch('createPerson', newFeaturePhoto);
clearTimeout(timeout);
isShowLoadingNewPerson = false;
dispatch('createPerson', newFeaturePhoto);
};
</script>
<section
transition:fly={{ x: 360, duration: 100, easing: linear }}
class="absolute top-0 z-[2001] h-full w-[360px] overflow-x-hidden p-2 bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg"
>
<div class="flex place-items-center justify-between gap-2">
{#if !searchFaces}
<div class="flex items-center gap-2">
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} />
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('select_face')}</p>
</div>
<div class="flex justify-end gap-2">
<CircleIconButton
icon={mdiMagnify}
title={$t('search_for_existing_person')}
on:click={() => {
searchFaces = true;
}}
/>
{#if !isShowLoadingNewPerson}
<CircleIconButton icon={mdiPlus} title={$t('create_new_person')} on:click={handleCreatePerson} />
{:else}
<div class="flex place-content-center place-items-center">
<LoadingSpinner />
</div>
{/if}
</div>
{:else}
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} />
<div class="w-full flex">
<SearchPeople
type="input"
bind:searchName
bind:showLoadingSpinner={isShowLoadingSearch}
bind:searchedPeopleLocal={searchedPeople}
/>
{#if isShowLoadingSearch}
<div>
<LoadingSpinner />
</div>
{/if}
</div>
<CircleIconButton icon={mdiClose} title={$t('cancel_search')} on:click={() => (searchFaces = false)} />
{/if}
</div>
<div class="px-4 py-4 text-sm">
<h2 class="mb-8 mt-4 uppercase">{$t('all_people')}</h2>
<div class="immich-scrollbar mt-4 flex flex-wrap gap-2 overflow-y-auto">
{#each showPeople as person (person.id)}
{#if !editedFace.person || person.id !== editedFace.person.id}
<div class="w-fit">
<button type="button" class="w-[90px]" on:click={() => dispatch('reassign', person)}>
<div class="relative">
<ImageThumbnail
curve
shadow
url={getPeopleThumbnailUrl(person)}
altText={$getPersonNameWithHiddenValue(person.name, person.isHidden)}
title={$getPersonNameWithHiddenValue(person.name, person.isHidden)}
widthStyle="90px"
heightStyle="90px"
hidden={person.isHidden}
/>
</div>
<p class="mt-1 truncate font-medium" title={$getPersonNameWithHiddenValue(person.name, person.isHidden)}>
{person.name}
</p>
</button>
</div>
{/if}
{/each}
</div>
</div>
</section>