mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
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:
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user