add faces

This commit is contained in:
Alex Tran 2023-08-13 10:11:22 -05:00
parent a7b62a5ad3
commit e2ad4ac5b3
4 changed files with 215 additions and 43 deletions

View file

@ -1,31 +1,65 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { PeopleResponseDto, PersonResponseDto, api } from '@api';
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
import PeopleCard from '$lib/components/faces-page/people-card.svelte';
import { PersonResponseDto, api } from '@api';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
import Button from '$lib/components/elements/buttons/button.svelte';
import FaceThumbnail from '$lib/components/assets/thumbnail/face-thumbnail.svelte';
export let selectedPeopleIds: string[] = [];
let people: PersonResponseDto[] = [];
let selectedPeople: PersonResponseDto[] = [];
const dispatch = createEventDispatcher<{ close: void; confirm: { people: PersonResponseDto[] } }>();
const dispatch = createEventDispatcher<{ close: void }>();
onMount(async () => {
const { data } = await api.personApi.getAllPeople({ withHidden: false });
people = data.people;
selectedPeople = people.filter((p) => selectedPeopleIds.includes(p.id));
});
const handleSelection = (e: CustomEvent<{ person: PersonResponseDto }>) => {
const person = e.detail.person;
if (selectedPeople.some((p) => p.id === person.id)) {
selectedPeople = selectedPeople.filter((p) => p.id !== person.id);
} else {
selectedPeople = [...selectedPeople, person];
}
};
const onConfirmClicked = () => {
dispatch('confirm', { people: selectedPeople });
};
</script>
<div class="">
<ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => dispatch('close')}>
<svelte:fragment slot="leading">
<p class="text-immich-fg dark:text-immich-dark-fg font-medium">Select a face</p>
</svelte:fragment>
</ControlAppBar>
<ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => dispatch('close')}>
<svelte:fragment slot="leading">
<p class="text-immich-fg dark:text-immich-dark-fg font-medium">
{#if selectedPeople.length === 0}
Select faces
{:else}
{selectedPeople.length} people selected
{/if}
</p>
</svelte:fragment>
<div class="mt-24 flex flex-wrap gap-2 px-8">
{#each people as person}
<PeopleCard {person} selectionMode disableContextMenu />
{/each}
</div>
<svelte:fragment slot="trailing">
<Button disabled={selectedPeople.length === 0} size="sm" title="Confirm" on:click={onConfirmClicked}>Confirm</Button
>
</svelte:fragment>
</ControlAppBar>
<div class="mt-24 flex flex-wrap gap-2 px-8">
{#each people as person}
<FaceThumbnail
{person}
thumbnailSize={180}
on:select={handleSelection}
on:click={handleSelection}
selected={selectedPeople.some((p) => p.id === person.id)}
/>
{/each}
</div>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import BaseModal from '$lib/components/shared-components/base-modal.svelte';
import { createEventDispatcher } from 'svelte';
import type { AlbumResponseDto } from '@api';
import { RuleKey, type AlbumResponseDto, type PersonResponseDto } from '@api';
import Plus from 'svelte-material-icons/Plus.svelte';
import Button from '../../elements/buttons/button.svelte';
import Portal from '../../shared-components/portal/portal.svelte';
@ -12,8 +12,17 @@
let peopleSelection = false;
let locationSelection = false;
$: selectedFaces = album.rules.map((r) => {
if (r.key === RuleKey.Person) {
return String(r.value);
}
}) as string[];
const dispatch = createEventDispatcher<{ close: void }>();
const handleFaceSelected = (e: CustomEvent<{ people: PersonResponseDto[] }>) => {
peopleSelection = false;
};
</script>
<BaseModal
@ -84,9 +93,13 @@
{#if peopleSelection}
<section
transition:fly={{ y: 500 }}
class="dark:bg-immich-dark-bg absolute left-0 top-0 z-[10000] h-full min-h-max w-full overflow-scroll bg-gray-200"
class="dark:bg-immich-dark-bg absolute left-0 top-0 z-[10000] h-full min-h-max w-full overflow-y-auto bg-gray-200"
>
<FaceSelection on:close={() => (peopleSelection = false)} />
<FaceSelection
on:close={() => (peopleSelection = false)}
on:confirm={handleFaceSelected}
selectedPeopleIds={selectedFaces}
/>
</section>
{/if}
</Portal>

View file

@ -0,0 +1,130 @@
<script lang="ts">
import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
import { api, PersonResponseDto } from '@api';
import { createEventDispatcher } from 'svelte';
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
import { fade } from 'svelte/transition';
import ImageThumbnail from './image-thumbnail.svelte';
export let person: PersonResponseDto;
export let groupIndex = 0;
export let thumbnailSize: number | undefined = undefined;
export let thumbnailWidth: number | undefined = undefined;
export let thumbnailHeight: number | undefined = undefined;
export let selected = false;
export let selectionCandidate = false;
export let disabled = false;
export let readonly = false;
let mouseOver = false;
const dispatch = createEventDispatcher<{
click: { person: PersonResponseDto };
select: { person: PersonResponseDto };
'mouse-event': { isMouseOver: boolean; selectedGroupIndex: number };
}>();
$: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
$: [width, height] = ((): [number, number] => {
if (thumbnailSize) {
return [thumbnailSize, thumbnailSize];
}
if (thumbnailWidth && thumbnailHeight) {
return [thumbnailWidth, thumbnailHeight];
}
return [235, 235];
})();
const thumbnailClickedHandler = () => {
if (!disabled) {
dispatch('click', { person });
}
};
const thumbnailKeyDownHandler = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
thumbnailClickedHandler();
}
};
const onIconClickedHandler = (e: MouseEvent) => {
e.stopPropagation();
if (!disabled) {
dispatch('select', { person });
}
};
</script>
<IntersectionObserver once={false} let:intersecting>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
style:width="{width}px"
style:height="{height}px"
class="group relative overflow-hidden {disabled
? 'bg-gray-300'
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
class:cursor-not-allowed={disabled}
class:hover:cursor-pointer={!disabled}
on:mouseenter={() => (mouseOver = true)}
on:mouseleave={() => (mouseOver = false)}
on:click={thumbnailClickedHandler}
on:keydown={thumbnailKeyDownHandler}
>
{#if intersecting}
<div class="absolute z-20 h-full w-full">
<!-- Select asset button -->
{#if !readonly && (mouseOver || selected || selectionCandidate)}
<button
on:click={onIconClickedHandler}
class="absolute p-2 focus:outline-none"
class:cursor-not-allowed={disabled}
role="checkbox"
aria-checked={selected}
{disabled}
>
{#if disabled}
<CheckCircle size="24" class="text-zinc-800" />
{:else if selected}
<div class="rounded-full bg-[#D9DCEF] dark:bg-[#232932]">
<CheckCircle size="24" class="text-immich-primary" />
</div>
{:else}
<CheckCircle size="24" class="text-white/80 hover:text-white" />
{/if}
</button>
{/if}
</div>
<div
class="dark:bg-immich-dark-gray absolute h-full w-full select-none bg-gray-100 transition-transform"
class:scale-[0.85]={selected}
class:rounded-xl={selected}
>
<!-- Gradient overlay on hover -->
<div
class="absolute z-10 h-full w-full bg-gradient-to-b from-black/25 via-[transparent_25%] opacity-0 transition-opacity group-hover:opacity-100"
class:rounded-xl={selected}
/>
<ImageThumbnail
url={api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
widthStyle="{width}px"
heightStyle="{height}px"
curve={selected}
/>
</div>
{#if selectionCandidate}
<div
class="bg-immich-primary absolute top-0 h-full w-full opacity-40"
in:fade={{ duration: 100 }}
out:fade={{ duration: 100 }}
/>
{/if}
{/if}
</div>
</IntersectionObserver>

View file

@ -9,11 +9,8 @@
import { createEventDispatcher } from 'svelte';
export let person: PersonResponseDto;
export let selectionMode = false;
export let disableContextMenu = false;
let showContextMenu = false;
let dispatch = createEventDispatcher();
const onChangeNameClicked = () => {
@ -30,7 +27,7 @@
</script>
<div id="people-card" class="relative">
<a href={!selectionMode ? '/people/{person.id}' : ''} draggable="false">
<a href="/people/{person.id}" draggable="false">
<div class="h-48 w-48 rounded-xl brightness-95 filter">
<ImageThumbnail shadow url={api.getPeopleThumbnailUrl(person.id)} altText={person.name} widthStyle="100%" />
</div>
@ -41,28 +38,26 @@
{/if}
</a>
{#if !disableContextMenu}
<button
class="absolute right-2 top-2 z-20"
on:click|stopPropagation|preventDefault={() => {
showContextMenu = !showContextMenu;
}}
data-testid="context-button-parent"
id={`icon-${person.id}`}
>
<IconButton color="transparent-primary">
<DotsVertical size="20" />
</IconButton>
<button
class="absolute right-2 top-2 z-20"
on:click|stopPropagation|preventDefault={() => {
showContextMenu = !showContextMenu;
}}
data-testid="context-button-parent"
id={`icon-${person.id}`}
>
<IconButton color="transparent-primary">
<DotsVertical size="20" />
</IconButton>
{#if showContextMenu}
<ContextMenu on:outclick={() => (showContextMenu = false)}>
<MenuOption on:click={() => onHideFaceClicked()} text="Hide face" />
<MenuOption on:click={() => onChangeNameClicked()} text="Change name" />
<MenuOption on:click={() => onMergeFacesClicked()} text="Merge faces" />
</ContextMenu>
{/if}
</button>
{/if}
{#if showContextMenu}
<ContextMenu on:outclick={() => (showContextMenu = false)}>
<MenuOption on:click={() => onHideFaceClicked()} text="Hide face" />
<MenuOption on:click={() => onChangeNameClicked()} text="Change name" />
<MenuOption on:click={() => onMergeFacesClicked()} text="Merge faces" />
</ContextMenu>
{/if}
</button>
</div>
{#if showContextMenu}