mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat: Mark people as favorite (#14866)
* feat: added ability to mark people as favorite, which get sorted to the front of the people list * feat(server): added unit test for favorite people * feat(server): refactored for better readability * fixed person service unit tests * fixed open-api and sql checks * fixed bad codegen and removed unnecessary type assertion again * chore: clean up --------- Co-authored-by: Alex <alex.tran1502@gmail.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
69e88ef985
commit
7ec3610753
20 changed files with 355 additions and 28 deletions
|
|
@ -1,4 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { focusOutside } from '$lib/actions/focus-outside';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { type PersonResponseDto } from '@immich/sdk';
|
||||
|
|
@ -8,12 +11,13 @@
|
|||
mdiCalendarEditOutline,
|
||||
mdiDotsVertical,
|
||||
mdiEyeOffOutline,
|
||||
mdiHeart,
|
||||
mdiHeartMinusOutline,
|
||||
mdiHeartOutline,
|
||||
} from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { focusOutside } from '$lib/actions/focus-outside';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
|
||||
interface Props {
|
||||
person: PersonResponseDto;
|
||||
|
|
@ -22,9 +26,18 @@
|
|||
onSetBirthDate: () => void;
|
||||
onMergePeople: () => void;
|
||||
onHidePerson: () => void;
|
||||
onToggleFavorite: () => void;
|
||||
}
|
||||
|
||||
let { person, preload = false, onChangeName, onSetBirthDate, onMergePeople, onHidePerson }: Props = $props();
|
||||
let {
|
||||
person,
|
||||
preload = false,
|
||||
onChangeName,
|
||||
onSetBirthDate,
|
||||
onMergePeople,
|
||||
onHidePerson,
|
||||
onToggleFavorite,
|
||||
}: Props = $props();
|
||||
|
||||
let showVerticalDots = $state(false);
|
||||
</script>
|
||||
|
|
@ -51,6 +64,11 @@
|
|||
title={person.name}
|
||||
widthStyle="100%"
|
||||
/>
|
||||
{#if person.isFavorite}
|
||||
<div class="absolute bottom-2 left-2 z-10">
|
||||
<Icon path={mdiHeart} size="24" class="text-white" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if person.name}
|
||||
<span
|
||||
|
|
@ -76,6 +94,11 @@
|
|||
<MenuOption onClick={onChangeName} icon={mdiAccountEditOutline} text={$t('change_name')} />
|
||||
<MenuOption onClick={onSetBirthDate} icon={mdiCalendarEditOutline} text={$t('set_date_of_birth')} />
|
||||
<MenuOption onClick={onMergePeople} icon={mdiAccountMultipleCheckOutline} text={$t('merge_people')} />
|
||||
<MenuOption
|
||||
onClick={onToggleFavorite}
|
||||
icon={person.isFavorite ? mdiHeartMinusOutline : mdiHeartOutline}
|
||||
text={person.isFavorite ? $t('unfavorite') : $t('to_favorite')}
|
||||
/>
|
||||
</ButtonContextMenu>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { mdiHeart } from '@mdi/js';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
|
|
@ -53,7 +55,7 @@
|
|||
<SingleGridRow class="grid md:grid-auto-fill-28 grid-auto-fill-20 gap-x-4">
|
||||
{#snippet children({ itemCount })}
|
||||
{#each people.slice(0, itemCount) as person (person.id)}
|
||||
<a href="{AppRoute.PEOPLE}/{person.id}" class="text-center">
|
||||
<a href="{AppRoute.PEOPLE}/{person.id}" class="text-center relative">
|
||||
<ImageThumbnail
|
||||
circle
|
||||
shadow
|
||||
|
|
@ -61,6 +63,11 @@
|
|||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
/>
|
||||
{#if person.isFavorite}
|
||||
<div class="absolute bottom-2 left-2 z-10">
|
||||
<Icon path={mdiHeart} size="24" class="text-white" />
|
||||
</div>
|
||||
{/if}
|
||||
<p class="mt-2 text-ellipsis text-sm font-medium dark:text-white">{person.name}</p>
|
||||
</a>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -222,6 +222,25 @@
|
|||
}
|
||||
};
|
||||
|
||||
const handleToggleFavorite = async (detail: PersonResponseDto) => {
|
||||
try {
|
||||
const updatedPerson = await updatePerson({
|
||||
id: detail.id,
|
||||
personUpdateDto: { isFavorite: !detail.isFavorite },
|
||||
});
|
||||
|
||||
const index = people.findIndex((person) => person.id === detail.id);
|
||||
people[index] = updatedPerson;
|
||||
|
||||
notificationController.show({
|
||||
message: updatedPerson.isFavorite ? $t('added_to_favorites') : $t('removed_from_favorites'),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: detail.isFavorite } }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMergePeople = async (detail: PersonResponseDto) => {
|
||||
await goto(
|
||||
`${AppRoute.PEOPLE}/${detail.id}?${QueryParameter.ACTION}=${ActionQueryParameterValue.MERGE}&${QueryParameter.PREVIOUS_ROUTE}=${AppRoute.PEOPLE}`,
|
||||
|
|
@ -413,6 +432,7 @@
|
|||
onSetBirthDate={() => handleSetBirthDate(person)}
|
||||
onMergePeople={() => handleMergePeople(person)}
|
||||
onHidePerson={() => handleHidePerson(person)}
|
||||
onToggleFavorite={() => handleToggleFavorite(person)}
|
||||
/>
|
||||
{/snippet}
|
||||
</PeopleInfiniteScroll>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { afterNavigate, goto, invalidateAll } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { clickOutside } from '$lib/actions/click-outside';
|
||||
import { listNavigation } from '$lib/actions/list-navigation';
|
||||
import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
|
||||
|
|
@ -17,8 +19,10 @@
|
|||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
|
||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
|
||||
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
|
|
@ -27,11 +31,12 @@
|
|||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { AssetStore } from '$lib/stores/assets.store';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||
import { clickOutside } from '$lib/actions/click-outside';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { isExternalUrl } from '$lib/utils/navigation';
|
||||
import {
|
||||
|
|
@ -50,16 +55,13 @@
|
|||
mdiDotsVertical,
|
||||
mdiEyeOffOutline,
|
||||
mdiEyeOutline,
|
||||
mdiHeartMinusOutline,
|
||||
mdiHeartOutline,
|
||||
mdiPlus,
|
||||
} from '@mdi/js';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { listNavigation } from '$lib/actions/list-navigation';
|
||||
import { t } from 'svelte-i18n';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
|
|
@ -181,6 +183,25 @@
|
|||
}
|
||||
};
|
||||
|
||||
const handleToggleFavorite = async () => {
|
||||
try {
|
||||
const updatedPerson = await updatePerson({
|
||||
id: person.id,
|
||||
personUpdateDto: { isFavorite: !person.isFavorite },
|
||||
});
|
||||
|
||||
// Invalidate to reload the page data and have the favorite status updated
|
||||
await invalidateAll();
|
||||
|
||||
notificationController.show({
|
||||
message: updatedPerson.isFavorite ? $t('added_to_favorites') : $t('removed_from_favorites'),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: person.isFavorite } }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMerge = async (person: PersonResponseDto) => {
|
||||
await updateAssetCount();
|
||||
await handleGoBack();
|
||||
|
|
@ -445,6 +466,11 @@
|
|||
icon={mdiAccountMultipleCheckOutline}
|
||||
onClick={() => (viewMode = PersonPageViewMode.MERGE_PEOPLE)}
|
||||
/>
|
||||
<MenuOption
|
||||
icon={person.isFavorite ? mdiHeartMinusOutline : mdiHeartOutline}
|
||||
text={person.isFavorite ? $t('unfavorite') : $t('to_favorite')}
|
||||
onClick={handleToggleFavorite}
|
||||
/>
|
||||
</ButtonContextMenu>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue