mirror of
https://github.com/immich-app/immich.git
synced 2026-04-29 20:40:38 -04:00
chore(web): refactor people panel (#28136)
This commit is contained in:
parent
081c75bb21
commit
b2b4385271
@ -11,7 +11,7 @@
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getAssetMediaUrl, getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { getAssetMediaUrl } from '$lib/utils';
|
||||
import { delay, getDimensions } from '$lib/utils/asset-utils';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@ -24,26 +24,15 @@
|
||||
type AssetResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { Icon, IconButton, LoadingSpinner, Text } from '@immich/ui';
|
||||
import {
|
||||
mdiCamera,
|
||||
mdiCameraIris,
|
||||
mdiClose,
|
||||
mdiEye,
|
||||
mdiEyeOff,
|
||||
mdiImageOutline,
|
||||
mdiInformationOutline,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
} from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { mdiCamera, mdiCameraIris, mdiClose, mdiImageOutline, mdiInformationOutline } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { slide } from 'svelte/transition';
|
||||
import ImageThumbnail from '../assets/thumbnail/ImageThumbnail.svelte';
|
||||
import PersonSidePanel from '../faces-page/PersonSidePanel.svelte';
|
||||
import OnEvents from '../OnEvents.svelte';
|
||||
import UserAvatar from '../shared-components/UserAvatar.svelte';
|
||||
import AlbumListItemDetails from './AlbumListItemDetails.svelte';
|
||||
import DetailPanelPeople from '$lib/components/asset-viewer/DetailPanelPeople.svelte';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
@ -53,8 +42,6 @@
|
||||
let { asset, currentAlbum = null }: Props = $props();
|
||||
|
||||
let isOwner = $derived(authManager.authenticated && authManager.user.id === asset.ownerId);
|
||||
let people = $derived(asset.people || []);
|
||||
let unassignedFaces = $derived(asset.unassignedFaces || []);
|
||||
let latlng = $derived(
|
||||
(() => {
|
||||
const lat = asset.exifInfo?.latitude;
|
||||
@ -162,110 +149,7 @@
|
||||
|
||||
<DetailPanelDescription {asset} {isOwner} />
|
||||
<DetailPanelRating {asset} {isOwner} />
|
||||
|
||||
{#if !authManager.isSharedLink && isOwner}
|
||||
<section class="px-4 pt-4 text-sm">
|
||||
<div class="flex h-10 w-full items-center justify-between">
|
||||
<Text size="small" color="muted">{$t('people')}</Text>
|
||||
<div class="flex gap-2 items-center">
|
||||
{#if people.some((person) => person.isHidden)}
|
||||
<IconButton
|
||||
aria-label={$t('show_hidden_people')}
|
||||
icon={assetViewerManager.isShowingHiddenPeople ? mdiEyeOff : mdiEye}
|
||||
size="medium"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => assetViewerManager.toggleHiddenPeople()}
|
||||
/>
|
||||
{/if}
|
||||
<IconButton
|
||||
aria-label={$t('tag_people')}
|
||||
icon={mdiPlus}
|
||||
size="medium"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => assetViewerManager.toggleFaceEditMode()}
|
||||
/>
|
||||
|
||||
{#if people.length > 0 || unassignedFaces.length > 0}
|
||||
<IconButton
|
||||
aria-label={$t('edit_people')}
|
||||
icon={mdiPencil}
|
||||
size="medium"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => assetViewerManager.openEditFacesPanel()}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
{#each people as person, index (person.id)}
|
||||
{#if assetViewerManager.isShowingHiddenPeople || !person.isHidden}
|
||||
{@const isHighlighted = people[index].faces.some((f) =>
|
||||
assetViewerManager.highlightedFaces.some((b) => b.id === f.id),
|
||||
)}
|
||||
<a
|
||||
class="group w-22 outline-none"
|
||||
href={Route.viewPerson(person, { previousRoute })}
|
||||
onfocus={() => assetViewerManager.setHighlightedFaces(people[index].faces)}
|
||||
onblur={() => assetViewerManager.clearHighlightedFaces()}
|
||||
onpointerenter={() => assetViewerManager.setHighlightedFaces(people[index].faces)}
|
||||
onpointerleave={() => assetViewerManager.clearHighlightedFaces()}
|
||||
>
|
||||
<div class="relative">
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person)}
|
||||
altText={person.name}
|
||||
title={person.name}
|
||||
widthStyle="90px"
|
||||
heightStyle="90px"
|
||||
hidden={person.isHidden}
|
||||
highlighted={isHighlighted}
|
||||
class="group-focus-visible:outline-2 group-focus-visible:outline-offset-2 group-focus-visible:outline-immich-primary dark:group-focus-visible:outline-immich-dark-primary"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 truncate font-medium" title={person.name}>{person.name}</p>
|
||||
{#if person.birthDate}
|
||||
{@const personBirthDate = DateTime.fromISO(person.birthDate)}
|
||||
{@const age = Math.floor(DateTime.fromISO(asset.localDateTime).diff(personBirthDate, 'years').years)}
|
||||
{@const ageInMonths = Math.floor(
|
||||
DateTime.fromISO(asset.localDateTime).diff(personBirthDate, 'months').months,
|
||||
)}
|
||||
{#if age >= 0}
|
||||
<p
|
||||
class="font-light"
|
||||
title={personBirthDate.toLocaleString(
|
||||
{
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
{ locale: $locale },
|
||||
)}
|
||||
>
|
||||
{#if ageInMonths <= 11}
|
||||
{$t('age_months', { values: { months: ageInMonths } })}
|
||||
{:else if ageInMonths > 12 && ageInMonths <= 23}
|
||||
{$t('age_year_months', { values: { months: ageInMonths - 12 } })}
|
||||
{:else}
|
||||
{$t('age_years', { values: { years: age } })}
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
<DetailPanelPeople {asset} {isOwner} {previousRoute} />
|
||||
|
||||
<div class="px-4 py-4">
|
||||
{#if asset.exifInfo}
|
||||
|
||||
133
web/src/lib/components/asset-viewer/DetailPanelPeople.svelte
Normal file
133
web/src/lib/components/asset-viewer/DetailPanelPeople.svelte
Normal file
@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/ImageThumbnail.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import { IconButton, Text } from '@immich/ui';
|
||||
import { mdiEye, mdiEyeOff, mdiPencil, mdiPlus } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
asset: AssetResponseDto;
|
||||
isOwner: boolean;
|
||||
previousRoute: string;
|
||||
};
|
||||
|
||||
const { asset, isOwner, previousRoute }: Props = $props();
|
||||
|
||||
const unassignedFaces = $derived(asset.unassignedFaces || []);
|
||||
const people = $derived(asset.people || []);
|
||||
const visiblePeople = $derived(
|
||||
people
|
||||
.filter((p) => assetViewerManager.isShowingHiddenPeople || !p.isHidden)
|
||||
.map((person) => {
|
||||
if (!person.birthDate) {
|
||||
return { formattedBirthDate: undefined, formattedAge: undefined, ...person };
|
||||
}
|
||||
const personBirthDate = DateTime.fromISO(person.birthDate);
|
||||
const ageInYears = Math.floor(DateTime.fromISO(asset.localDateTime).diff(personBirthDate, 'years').years);
|
||||
const ageInMonths = Math.floor(DateTime.fromISO(asset.localDateTime).diff(personBirthDate, 'months').months);
|
||||
|
||||
let formattedAge;
|
||||
if (ageInYears < 0) {
|
||||
return { formattedBirthDate: undefined, formattedAge: undefined, ...person };
|
||||
} else if (ageInMonths < 12) {
|
||||
formattedAge = $t('age_months', { values: { months: ageInMonths } });
|
||||
} else if (ageInMonths > 12 && ageInMonths < 24) {
|
||||
formattedAge = $t('age_year_months', { values: { months: ageInMonths - 12 } });
|
||||
} else {
|
||||
formattedAge = $t('age_years', { values: { years: ageInYears } });
|
||||
}
|
||||
|
||||
const formattedBirthDate = personBirthDate.toLocaleString(
|
||||
{
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
{ locale: $locale },
|
||||
);
|
||||
return { formattedBirthDate, formattedAge, ...person };
|
||||
}),
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if !authManager.isSharedLink && isOwner}
|
||||
<section class="px-4 pt-4 text-sm">
|
||||
<div class="flex h-10 w-full items-center justify-between">
|
||||
<Text size="small" color="muted">{$t('people')}</Text>
|
||||
<div class="flex gap-2 items-center">
|
||||
{#if people.some((person) => person.isHidden)}
|
||||
<IconButton
|
||||
aria-label={$t('show_hidden_people')}
|
||||
icon={assetViewerManager.isShowingHiddenPeople ? mdiEyeOff : mdiEye}
|
||||
size="medium"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => assetViewerManager.toggleHiddenPeople()}
|
||||
/>
|
||||
{/if}
|
||||
<IconButton
|
||||
aria-label={$t('tag_people')}
|
||||
icon={mdiPlus}
|
||||
size="medium"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => assetViewerManager.toggleFaceEditMode()}
|
||||
/>
|
||||
|
||||
{#if people.length > 0 || unassignedFaces.length > 0}
|
||||
<IconButton
|
||||
aria-label={$t('edit_people')}
|
||||
icon={mdiPencil}
|
||||
size="medium"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => assetViewerManager.openEditFacesPanel()}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 grid {visiblePeople.length <= 6 ? 'grid-cols-3 gap-3' : 'grid-cols-4 gap-2'}">
|
||||
{#each visiblePeople as person (person.id)}
|
||||
{@const isHighlighted = person.faces.some((f) =>
|
||||
assetViewerManager.highlightedFaces.some((b) => b.id === f.id),
|
||||
)}
|
||||
<a
|
||||
class="group outline-none"
|
||||
href={Route.viewPerson(person, { previousRoute })}
|
||||
onfocus={() => assetViewerManager.setHighlightedFaces(person.faces)}
|
||||
onblur={() => assetViewerManager.clearHighlightedFaces()}
|
||||
onpointerenter={() => assetViewerManager.setHighlightedFaces(person.faces)}
|
||||
onpointerleave={() => assetViewerManager.clearHighlightedFaces()}
|
||||
>
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person)}
|
||||
altText={person.name}
|
||||
title={person.name}
|
||||
widthStyle="100%"
|
||||
hidden={person.isHidden}
|
||||
highlighted={isHighlighted}
|
||||
class="group-focus-visible:outline-2 outline-offset-2 outline-immich-primary dark:outline-immich-dark-primary"
|
||||
/>
|
||||
<p class="mt-1 truncate font-medium" title={person.name}>{person.name}</p>
|
||||
{#if person.birthDate && person.formattedAge}
|
||||
<p class="font-light {visiblePeople.length > 6 ? 'text-xs' : ''}" title={person.formattedBirthDate!}>
|
||||
{person.formattedAge}
|
||||
</p>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
Loading…
x
Reference in New Issue
Block a user