feat(server,web): hide faces (#3262)

* feat: hide faces

* fix: types

* pr feedback

* fix: svelte checks

* feat: new server endpoint

* refactor: rename person count dto

* fix(server): linter

* fix: remove duplicate button

* docs: add comments

* pr feedback

* fix: get unhidden faces

* fix: do not use PersonCountResponseDto

* fix: transition

* pr feedback

* pr feedback

* fix: remove unused check

* add server tests

* rename persons to people

* feat: add exit button

* pr feedback

* add server tests

* pr feedback

* pr feedback

* fix: show & hide faces

* simplify

* fix: close button

* pr feeback

* pr feeback

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
martin
2023-07-18 20:09:43 +02:00
committed by GitHub
parent 02b70e693c
commit f28fc8fa5c
38 changed files with 742 additions and 108 deletions

View File

@@ -9,11 +9,11 @@ export const load = (async ({ locals, parent }) => {
}
const { data: items } = await locals.api.searchApi.getExploreData();
const { data: people } = await locals.api.personApi.getAllPeople();
const { data: response } = await locals.api.personApi.getAllPeople({ withHidden: false });
return {
user,
items,
people,
response,
meta: {
title: 'Explore',
},

View File

@@ -19,7 +19,6 @@
}
const MAX_ITEMS = 12;
const getFieldItems = (items: SearchExploreResponseDto[], field: Field) => {
const targetField = items.find((item) => item.fieldName === field);
return targetField?.items || [];
@@ -27,21 +26,20 @@
$: things = getFieldItems(data.items, Field.OBJECTS);
$: places = getFieldItems(data.items, Field.CITY);
$: people = data.people.slice(0, MAX_ITEMS);
$: people = data.response.people.slice(0, MAX_ITEMS);
$: hasPeople = data.response.total > 0;
</script>
<UserPageLayout user={data.user} title={data.meta.title}>
{#if people.length > 0}
{#if hasPeople}
<div class="mb-6 mt-2">
<div class="flex justify-between">
<p class="mb-4 dark:text-immich-dark-fg font-medium">People</p>
{#if data.people.length > MAX_ITEMS}
<a
href={AppRoute.PEOPLE}
class="font-medium text-sm pr-4 hover:text-immich-primary dark:hover:text-immich-dark-primary dark:text-immich-dark-fg"
draggable="false">View All</a
>
{/if}
<a
href={AppRoute.PEOPLE}
class="font-medium text-sm pr-4 hover:text-immich-primary dark:hover:text-immich-dark-primary dark:text-immich-dark-fg"
draggable="false">View All</a
>
</div>
<div class="flex flex-row flex-wrap gap-4">
{#each people as person (person.id)}

View File

@@ -8,8 +8,7 @@ export const load = (async ({ locals, parent }) => {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const { data: people } = await locals.api.personApi.getAllPeople();
const { data: people } = await locals.api.personApi.getAllPeople({ withHidden: true });
return {
user,
people,

View File

@@ -6,14 +6,74 @@
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import Button from '$lib/components/elements/buttons/button.svelte';
import { api, type PersonResponseDto } from '@api';
import { handleError } from '$lib/utils/handle-error';
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error';
import {
NotificationType,
notificationController,
} from '$lib/components/shared-components/notification/notification';
import ShowHide from '$lib/components/faces-page/show-hide.svelte';
import IconButton from '$lib/components/elements/buttons/icon-button.svelte';
import EyeOutline from 'svelte-material-icons/EyeOutline.svelte';
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
export let data: PageData;
let selectHidden = false;
let changeCounter = 0;
let initialHiddenValues: Record<string, boolean> = {};
let people = data.people.people;
let countTotalPeople = data.people.total;
let countVisiblePeople = data.people.visible;
people.forEach((person: PersonResponseDto) => {
initialHiddenValues[person.id] = person.isHidden;
});
const handleCloseClick = () => {
selectHidden = false;
people.forEach((person: PersonResponseDto) => {
person.isHidden = initialHiddenValues[person.id];
});
};
const handleDoneClick = async () => {
selectHidden = false;
try {
// Reset the counter before checking changes
let changeCounter = 0;
// Check if the visibility for each person has been changed
for (const person of people) {
if (person.isHidden !== initialHiddenValues[person.id]) {
changeCounter++;
await api.personApi.updatePerson({
id: person.id,
personUpdateDto: { isHidden: person.isHidden },
});
// Update the initial hidden values
initialHiddenValues[person.id] = person.isHidden;
// Update the count of hidden/visible people
countVisiblePeople += person.isHidden ? -1 : 1;
}
}
if (changeCounter > 0) {
notificationController.show({
type: NotificationType.Info,
message: `Visibility changed for ${changeCounter} ${changeCounter <= 1 ? 'person' : 'people'}`,
});
}
} catch (error) {
handleError(
error,
`Unable to change the visibility for ${changeCounter} ${changeCounter <= 1 ? 'person' : 'people'}`,
);
}
};
let showChangeNameModal = false;
let personName = '';
@@ -37,7 +97,7 @@
personUpdateDto: { name: personName },
});
data.people = data.people.map((person: PersonResponseDto) => {
people = people.map((person: PersonResponseDto) => {
if (person.id === updatedPerson.id) {
return updatedPerson;
}
@@ -57,35 +117,48 @@
};
</script>
<UserPageLayout user={data.user} showUploadButton title="People">
<section>
{#if data.people.length > 0}
<div class="pl-4">
<div class="flex flex-row flex-wrap gap-1">
{#each data.people as person (person.id)}
<PeopleCard {person} on:change-name={handleChangeName} on:merge-faces={handleMergeFaces} />
{/each}
<UserPageLayout user={data.user} title="People">
<svelte:fragment slot="buttons">
{#if countTotalPeople > 0}
<IconButton on:click={() => (selectHidden = !selectHidden)}>
<div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
<EyeOutline size="18" />
<p class="ml-2">Show & hide faces</p>
</div>
</div>
{:else}
<div class="flex items-center place-content-center w-full min-h-[calc(66vh_-_11rem)] dark:text-white">
<div class="flex flex-col content-center items-center text-center">
<AccountOff size="3.5em" />
<p class="font-medium text-3xl mt-5">No people</p>
</div>
</div>
</IconButton>
{/if}
</section>
</svelte:fragment>
{#if countVisiblePeople > 0}
<div class="pl-4">
<div class="flex flex-row flex-wrap gap-1">
{#key selectHidden}
{#each people as person (person.id)}
{#if !person.isHidden}
<PeopleCard {person} on:change-name={handleChangeName} on:merge-faces={handleMergeFaces} />
{/if}
{/each}
{/key}
</div>
</div>
{:else}
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
<div class="flex flex-col content-center items-center text-center">
<AccountOff size="3.5em" />
<p class="mt-5 text-3xl font-medium">No people</p>
</div>
</div>
{/if}
{#if showChangeNameModal}
<FullScreenModal on:clickOutside={() => (showChangeNameModal = false)}>
<div
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-3xl py-8 dark:text-immich-dark-fg"
class="bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray dark:text-immich-dark-fg w-[500px] max-w-[95vw] rounded-3xl border p-4 py-8 shadow-sm"
>
<div
class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
class="text-immich-primary dark:text-immich-dark-primary flex flex-col place-content-center place-items-center gap-4 px-4"
>
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">Change name</h1>
<h1 class="text-immich-primary dark:text-immich-dark-primary text-2xl font-medium">Change name</h1>
</div>
<form on:submit|preventDefault={submitNameChange} autocomplete="off">
@@ -95,7 +168,7 @@
<input class="immich-form-input" id="name" name="name" type="text" bind:value={personName} autofocus />
</div>
<div class="flex w-full px-4 gap-4 mt-8">
<div class="mt-8 flex w-full gap-4 px-4">
<Button
color="gray"
fullwidth
@@ -110,3 +183,33 @@
</FullScreenModal>
{/if}
</UserPageLayout>
{#if selectHidden}
<ShowHide on:doneClick={handleDoneClick} on:closeClick={handleCloseClick}>
<div class="pl-4">
<div class="flex flex-row flex-wrap gap-1">
{#each people as person (person.id)}
<div class="relative">
<div class="h-48 w-48 rounded-xl brightness-95 filter">
<button class="h-full w-full" on:click={() => (person.isHidden = !person.isHidden)}>
<ImageThumbnail
bind:hidden={person.isHidden}
shadow
url={api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
widthStyle="100%"
/>
</button>
</div>
{#if person.name}
<span
class="w-100 absolute bottom-2 w-full text-ellipsis px-1 text-center font-medium text-white backdrop-blur-[1px] hover:cursor-pointer"
>
{person.name}
</span>
{/if}
</div>
{/each}
</div>
</div>
</ShowHide>
{/if}