mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
feat: added ability to mark people as favorite, which get sorted to the front of the people list
This commit is contained in:
parent
4bc2aa5451
commit
87fa163bdd
BIN
mobile/openapi/lib/model/people_update_item.dart
generated
BIN
mobile/openapi/lib/model/people_update_item.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/person_create_dto.dart
generated
BIN
mobile/openapi/lib/model/person_create_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/person_response_dto.dart
generated
BIN
mobile/openapi/lib/model/person_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/person_update_dto.dart
generated
BIN
mobile/openapi/lib/model/person_update_dto.dart
generated
Binary file not shown.
Binary file not shown.
@ -10182,6 +10182,9 @@
|
||||
"description": "Person id.",
|
||||
"type": "string"
|
||||
},
|
||||
"isFavorite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isHidden": {
|
||||
"description": "Person visibility",
|
||||
"type": "boolean"
|
||||
@ -10287,6 +10290,9 @@
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"isFavorite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isHidden": {
|
||||
"description": "Person visibility",
|
||||
"type": "boolean"
|
||||
@ -10308,6 +10314,10 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isFavorite": {
|
||||
"description": "This property was added in DEV",
|
||||
"type": "boolean"
|
||||
},
|
||||
"isHidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@ -10355,6 +10365,9 @@
|
||||
"description": "Asset is used to get the feature face thumbnail.",
|
||||
"type": "string"
|
||||
},
|
||||
"isFavorite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isHidden": {
|
||||
"description": "Person visibility",
|
||||
"type": "boolean"
|
||||
@ -10382,6 +10395,10 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isFavorite": {
|
||||
"description": "This property was added in DEV",
|
||||
"type": "boolean"
|
||||
},
|
||||
"isHidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
@ -215,6 +215,8 @@ export type PersonWithFacesResponseDto = {
|
||||
birthDate: string | null;
|
||||
faces: AssetFaceWithoutPersonResponseDto[];
|
||||
id: string;
|
||||
/** This property was added in DEV */
|
||||
isFavorite?: boolean;
|
||||
isHidden: boolean;
|
||||
name: string;
|
||||
thumbnailPath: string;
|
||||
@ -492,6 +494,8 @@ export type DuplicateResponseDto = {
|
||||
export type PersonResponseDto = {
|
||||
birthDate: string | null;
|
||||
id: string;
|
||||
/** This property was added in DEV */
|
||||
isFavorite?: boolean;
|
||||
isHidden: boolean;
|
||||
name: string;
|
||||
thumbnailPath: string;
|
||||
@ -689,6 +693,7 @@ export type PersonCreateDto = {
|
||||
/** Person date of birth.
|
||||
Note: the mobile app cannot currently set the birth date to null. */
|
||||
birthDate?: string | null;
|
||||
isFavorite?: boolean;
|
||||
/** Person visibility */
|
||||
isHidden?: boolean;
|
||||
/** Person name. */
|
||||
@ -702,6 +707,7 @@ export type PeopleUpdateItem = {
|
||||
featureFaceAssetId?: string;
|
||||
/** Person id. */
|
||||
id: string;
|
||||
isFavorite?: boolean;
|
||||
/** Person visibility */
|
||||
isHidden?: boolean;
|
||||
/** Person name. */
|
||||
@ -716,6 +722,7 @@ export type PersonUpdateDto = {
|
||||
birthDate?: string | null;
|
||||
/** Asset is used to get the feature face thumbnail. */
|
||||
featureFaceAssetId?: string;
|
||||
isFavorite?: boolean;
|
||||
/** Person visibility */
|
||||
isHidden?: boolean;
|
||||
/** Person name. */
|
||||
|
@ -32,6 +32,10 @@ export class PersonCreateDto {
|
||||
*/
|
||||
@ValidateBoolean({ optional: true })
|
||||
isHidden?: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@ValidateBoolean({ optional: true })
|
||||
isFavorite?: boolean;
|
||||
}
|
||||
|
||||
export class PersonUpdateDto extends PersonCreateDto {
|
||||
@ -97,6 +101,9 @@ export class PersonResponseDto {
|
||||
isHidden!: boolean;
|
||||
@PropertyLifecycle({ addedAt: 'v1.107.0' })
|
||||
updatedAt?: Date;
|
||||
@ApiProperty()
|
||||
@PropertyLifecycle({ addedAt: 'DEV' })
|
||||
isFavorite?: boolean;
|
||||
}
|
||||
|
||||
export class PersonWithFacesResponseDto extends PersonResponseDto {
|
||||
@ -170,6 +177,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
|
||||
birthDate: person.birthDate,
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
isHidden: person.isHidden,
|
||||
isFavorite: person.isFavorite,
|
||||
updatedAt: person.updatedAt,
|
||||
};
|
||||
}
|
||||
|
@ -49,4 +49,7 @@ export class PersonEntity {
|
||||
|
||||
@Column({ default: false })
|
||||
isHidden!: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
isFavorite!: boolean;
|
||||
}
|
||||
|
14
server/src/migrations/1734879118272-AddIsFavoritePerson.ts
Normal file
14
server/src/migrations/1734879118272-AddIsFavoritePerson.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddIsFavoritePerson1734879118272 implements MigrationInterface {
|
||||
name = 'AddIsFavoritePerson1734879118272'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "person" ADD "isFavorite" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "isFavorite"`);
|
||||
}
|
||||
|
||||
}
|
@ -95,6 +95,7 @@ export class PersonRepository implements IPersonRepository {
|
||||
.innerJoin('face.asset', 'asset')
|
||||
.andWhere('asset.isArchived = false')
|
||||
.orderBy('person.isHidden', 'ASC')
|
||||
.addOrderBy('person.isFavorite', 'DESC')
|
||||
.addOrderBy("NULLIF(person.name, '') IS NULL", 'ASC')
|
||||
.addOrderBy('COUNT(face.assetId)', 'DESC')
|
||||
.addOrderBy("NULLIF(person.name, '')", 'ASC', 'NULLS LAST')
|
||||
|
@ -185,13 +185,14 @@ export class PersonService extends BaseService {
|
||||
name: dto.name,
|
||||
birthDate: dto.birthDate,
|
||||
isHidden: dto.isHidden,
|
||||
isFavorite: dto.isFavorite,
|
||||
});
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
||||
|
||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
|
||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite } = dto;
|
||||
// TODO: set by faceId directly
|
||||
let faceId: string | undefined = undefined;
|
||||
if (assetId) {
|
||||
@ -204,7 +205,14 @@ export class PersonService extends BaseService {
|
||||
faceId = face.id;
|
||||
}
|
||||
|
||||
const person = await this.personRepository.update({ id, faceAssetId: faceId, name, birthDate, isHidden });
|
||||
const person = await this.personRepository.update({
|
||||
id,
|
||||
faceAssetId: faceId,
|
||||
name,
|
||||
birthDate,
|
||||
isHidden,
|
||||
isFavorite,
|
||||
});
|
||||
|
||||
if (assetId) {
|
||||
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
|
||||
@ -222,6 +230,7 @@ export class PersonService extends BaseService {
|
||||
name: person.name,
|
||||
birthDate: person.birthDate,
|
||||
featureFaceAssetId: person.featureFaceAssetId,
|
||||
isFavorite: person.isFavorite,
|
||||
});
|
||||
results.push({ id: person.id, success: true });
|
||||
} catch (error: Error | any) {
|
||||
|
@ -8,12 +8,15 @@
|
||||
mdiCalendarEditOutline,
|
||||
mdiDotsVertical,
|
||||
mdiEyeOffOutline,
|
||||
mdiHeart,
|
||||
mdiHeartOutline,
|
||||
} from '@mdi/js';
|
||||
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';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
|
||||
interface Props {
|
||||
person: PersonResponseDto;
|
||||
@ -22,9 +25,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 +63,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 +93,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 ? mdiHeart : 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}
|
||||
|
@ -223,6 +223,29 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleFavorite = async (detail: PersonResponseDto) => {
|
||||
try {
|
||||
const updatedPerson = await updatePerson({
|
||||
id: detail.id,
|
||||
personUpdateDto: { isFavorite: !detail.isFavorite },
|
||||
});
|
||||
|
||||
people = people.map((person: PersonResponseDto) => {
|
||||
if (person.id === updatedPerson.id) {
|
||||
return updatedPerson;
|
||||
}
|
||||
return person;
|
||||
});
|
||||
|
||||
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 +436,7 @@
|
||||
onSetBirthDate={() => handleSetBirthDate(person)}
|
||||
onMergePeople={() => handleMergePeople(person)}
|
||||
onHidePerson={() => handleHidePerson(person)}
|
||||
onToggleFavorite={() => handleToggleFavorite(person)}
|
||||
/>
|
||||
{/snippet}
|
||||
</PeopleInfiniteScroll>
|
||||
|
Loading…
Reference in New Issue
Block a user