0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-21 00:52:43 -05:00

fix(web): new feature photo (#9443)

* fix: new feature photo

* fix: use updatedAt
This commit is contained in:
martin 2024-06-28 02:16:26 +02:00 committed by GitHub
parent 325aa1d392
commit 37b5d92110
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 89 additions and 31 deletions

View file

@ -18,6 +18,7 @@ class PersonResponseDto {
required this.isHidden,
required this.name,
required this.thumbnailPath,
this.updatedAt,
});
DateTime? birthDate;
@ -30,13 +31,23 @@ class PersonResponseDto {
String thumbnailPath;
/// This property was added in v1.107.0
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
DateTime? updatedAt;
@override
bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto &&
other.birthDate == birthDate &&
other.id == id &&
other.isHidden == isHidden &&
other.name == name &&
other.thumbnailPath == thumbnailPath;
other.thumbnailPath == thumbnailPath &&
other.updatedAt == updatedAt;
@override
int get hashCode =>
@ -45,10 +56,11 @@ class PersonResponseDto {
(id.hashCode) +
(isHidden.hashCode) +
(name.hashCode) +
(thumbnailPath.hashCode);
(thumbnailPath.hashCode) +
(updatedAt == null ? 0 : updatedAt!.hashCode);
@override
String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath]';
String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -61,6 +73,11 @@ class PersonResponseDto {
json[r'isHidden'] = this.isHidden;
json[r'name'] = this.name;
json[r'thumbnailPath'] = this.thumbnailPath;
if (this.updatedAt != null) {
json[r'updatedAt'] = this.updatedAt!.toUtc().toIso8601String();
} else {
// json[r'updatedAt'] = null;
}
return json;
}
@ -77,6 +94,7 @@ class PersonResponseDto {
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
name: mapValueOfType<String>(json, r'name')!,
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
updatedAt: mapDateTime(json, r'updatedAt', r''),
);
}
return null;

View file

@ -19,6 +19,7 @@ class PersonWithFacesResponseDto {
required this.isHidden,
required this.name,
required this.thumbnailPath,
this.updatedAt,
});
DateTime? birthDate;
@ -33,6 +34,15 @@ class PersonWithFacesResponseDto {
String thumbnailPath;
/// This property was added in v1.107.0
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
DateTime? updatedAt;
@override
bool operator ==(Object other) => identical(this, other) || other is PersonWithFacesResponseDto &&
other.birthDate == birthDate &&
@ -40,7 +50,8 @@ class PersonWithFacesResponseDto {
other.id == id &&
other.isHidden == isHidden &&
other.name == name &&
other.thumbnailPath == thumbnailPath;
other.thumbnailPath == thumbnailPath &&
other.updatedAt == updatedAt;
@override
int get hashCode =>
@ -50,10 +61,11 @@ class PersonWithFacesResponseDto {
(id.hashCode) +
(isHidden.hashCode) +
(name.hashCode) +
(thumbnailPath.hashCode);
(thumbnailPath.hashCode) +
(updatedAt == null ? 0 : updatedAt!.hashCode);
@override
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath]';
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -67,6 +79,11 @@ class PersonWithFacesResponseDto {
json[r'isHidden'] = this.isHidden;
json[r'name'] = this.name;
json[r'thumbnailPath'] = this.thumbnailPath;
if (this.updatedAt != null) {
json[r'updatedAt'] = this.updatedAt!.toUtc().toIso8601String();
} else {
// json[r'updatedAt'] = null;
}
return json;
}
@ -84,6 +101,7 @@ class PersonWithFacesResponseDto {
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
name: mapValueOfType<String>(json, r'name')!,
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
updatedAt: mapDateTime(json, r'updatedAt', r''),
);
}
return null;

View file

@ -9345,6 +9345,11 @@
},
"thumbnailPath": {
"type": "string"
},
"updatedAt": {
"description": "This property was added in v1.107.0",
"format": "date-time",
"type": "string"
}
},
"required": [
@ -9414,6 +9419,11 @@
},
"thumbnailPath": {
"type": "string"
},
"updatedAt": {
"description": "This property was added in v1.107.0",
"format": "date-time",
"type": "string"
}
},
"required": [

View file

@ -158,6 +158,8 @@ export type PersonWithFacesResponseDto = {
isHidden: boolean;
name: string;
thumbnailPath: string;
/** This property was added in v1.107.0 */
updatedAt?: string;
};
export type SmartInfoResponseDto = {
objects?: string[] | null;
@ -432,6 +434,8 @@ export type PersonResponseDto = {
isHidden: boolean;
name: string;
thumbnailPath: string;
/** This property was added in v1.107.0 */
updatedAt?: string;
};
export type AssetFaceResponseDto = {
boundingBoxX1: number;

View file

@ -1,6 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsNotEmpty, IsString, MaxDate, ValidateNested } from 'class-validator';
import { PropertyLifecycle } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { PersonEntity } from 'src/entities/person.entity';
@ -71,6 +72,8 @@ export class PersonResponseDto {
birthDate!: Date | null;
thumbnailPath!: string;
isHidden!: boolean;
@PropertyLifecycle({ addedAt: 'v1.107.0' })
updatedAt?: Date;
}
export class PersonWithFacesResponseDto extends PersonResponseDto {
@ -138,6 +141,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
birthDate: person.birthDate,
thumbnailPath: person.thumbnailPath,
isHidden: person.isHidden,
updatedAt: person.updatedAt,
};
}

View file

@ -42,6 +42,7 @@ const responseDto: PersonResponseDto = {
birthDate: null,
thumbnailPath: '/path/to/thumbnail.jpg',
isHidden: false,
updatedAt: expect.any(Date),
};
const statistics = { assets: 3 };
@ -126,6 +127,7 @@ describe(PersonService.name, () => {
birthDate: null,
thumbnailPath: '/path/to/thumbnail.jpg',
isHidden: true,
updatedAt: expect.any(Date),
},
],
});
@ -255,6 +257,7 @@ describe(PersonService.name, () => {
birthDate: new Date('1976-06-30'),
thumbnailPath: '/path/to/thumbnail.jpg',
isHidden: false,
updatedAt: expect.any(Date),
});
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') });
expect(jobMock.queue).not.toHaveBeenCalled();
@ -407,6 +410,7 @@ describe(PersonService.name, () => {
id: personStub.noName.id,
name: personStub.noName.name,
thumbnailPath: personStub.noName.thumbnailPath,
updatedAt: expect.any(Date),
});
expect(jobMock.queue).not.toHaveBeenCalledWith();

View file

@ -213,7 +213,7 @@
<ImageThumbnail
curve
shadow
url={getPeopleThumbnailUrl(person.id)}
url={getPeopleThumbnailUrl(person)}
altText={person.name}
title={person.name}
widthStyle="90px"

View file

@ -108,7 +108,7 @@
<ImageThumbnail
curve
shadow
url={getPeopleThumbnailUrl(person.id)}
url={getPeopleThumbnailUrl(person)}
altText={getPersonNameWithHiddenValue(person.name, person.isHidden)}
title={getPersonNameWithHiddenValue(person.name, person.isHidden)}
widthStyle="90px"

View file

@ -36,7 +36,7 @@
class:dark:border-immich-dark-primary={border}
class:border-immich-primary={border}
>
<ImageThumbnail {circle} url={getPeopleThumbnailUrl(person.id)} altText={person.name} widthStyle="100%" shadow />
<ImageThumbnail {circle} url={getPeopleThumbnailUrl(person)} altText={person.name} widthStyle="100%" shadow />
</div>
<div

View file

@ -38,7 +38,7 @@
<ImageThumbnail
circle
shadow
url={getPeopleThumbnailUrl(personMerge1.id)}
url={getPeopleThumbnailUrl(personMerge1)}
altText={personMerge1.name}
widthStyle="100%"
/>
@ -65,7 +65,7 @@
border={potentialMergePeople.length > 0}
circle
shadow
url={getPeopleThumbnailUrl(personMerge2.id)}
url={getPeopleThumbnailUrl(personMerge2)}
altText={personMerge2.name}
widthStyle="100%"
/>
@ -84,7 +84,7 @@
border={true}
circle
shadow
url={getPeopleThumbnailUrl(person.id)}
url={getPeopleThumbnailUrl(person)}
altText={person.name}
widthStyle="100%"
on:click={() => changePersonToMerge(person)}

View file

@ -50,7 +50,7 @@
<ImageThumbnail
shadow
{preload}
url={getPeopleThumbnailUrl(person.id)}
url={getPeopleThumbnailUrl(person)}
altText={person.name}
title={person.name}
widthStyle="100%"

View file

@ -234,7 +234,7 @@
<ImageThumbnail
curve
shadow
url={getPeopleThumbnailUrl(selectedPersonToReassign[face.id].id)}
url={getPeopleThumbnailUrl(selectedPersonToReassign[face.id])}
altText={selectedPersonToReassign[face.id].name}
title={getPersonNameWithHiddenValue(
selectedPersonToReassign[face.id].name,
@ -248,7 +248,7 @@
<ImageThumbnail
curve
shadow
url={getPeopleThumbnailUrl(face.person.id)}
url={getPeopleThumbnailUrl(face.person)}
altText={face.person.name}
title={getPersonNameWithHiddenValue(face.person.name, face.person.isHidden)}
widthStyle={thumbnailWidth}

View file

@ -71,13 +71,7 @@
: 'border-transparent'}"
on:click={() => togglePersonSelection(person.id)}
>
<ImageThumbnail
circle
shadow
url={getPeopleThumbnailUrl(person.id)}
altText={person.name}
widthStyle="100%"
/>
<ImageThumbnail circle shadow url={getPeopleThumbnailUrl(person)} altText={person.name} widthStyle="100%" />
<p class="mt-2 line-clamp-2 text-sm font-medium dark:text-white">{person.name}</p>
</button>
{/each}

View file

@ -621,6 +621,7 @@
"unable_to_save_settings": "Unable to save settings",
"unable_to_scan_libraries": "Unable to scan libraries",
"unable_to_scan_library": "Unable to scan library",
"unable_to_set_feature_photo": "Unable to set feature photo",
"unable_to_set_profile_picture": "Unable to set profile picture",
"unable_to_submit_job": "Unable to submit job",
"unable_to_trash_asset": "Unable to trash asset",

View file

@ -17,6 +17,7 @@ import {
startOAuth,
unlinkOAuthAccount,
type AssetResponseDto,
type PersonResponseDto,
type SharedLinkResponseDto,
} from '@immich/sdk';
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js';
@ -205,7 +206,8 @@ export const getAssetPlaybackUrl = (options: string | { id: string; checksum?: s
export const getProfileImageUrl = (userId: string) => createUrl(getUserProfileImagePath(userId));
export const getPeopleThumbnailUrl = (personId: string) => createUrl(getPeopleThumbnailPath(personId));
export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) =>
createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt });
export const getAssetJobName = derived(t, ($t) => {
return (job: AssetJobName) => {

View file

@ -61,7 +61,7 @@
<ImageThumbnail
circle
shadow
url={getPeopleThumbnailUrl(person.id)}
url={getPeopleThumbnailUrl(person)}
altText={person.name}
widthStyle="100%"
/>

View file

@ -508,7 +508,7 @@
preload={searchName !== '' || index < 20}
bind:hidden={person.isHidden}
shadow
url={getPeopleThumbnailUrl(person.id)}
url={getPeopleThumbnailUrl(person)}
altText={person.name}
widthStyle="100%"
bind:eyeColor={eyeColorMap[person.id]}

View file

@ -91,7 +91,7 @@
let refreshAssetGrid = false;
let personName = '';
$: thumbnailData = getPeopleThumbnailUrl(data.person.id);
$: thumbnailData = getPeopleThumbnailUrl(data.person);
let name: string = data.person.name;
let suggestedPeople: PersonResponseDto[] = [];
@ -121,7 +121,7 @@
return websocketEvents.on('on_person_thumbnail', (personId: string) => {
if (data.person.id === personId) {
thumbnailData = getPeopleThumbnailUrl(data.person.id) + `?now=${Date.now()}`;
thumbnailData = getPeopleThumbnailUrl(data.person, Date.now().toString());
}
});
});
@ -206,10 +206,13 @@
if (viewMode !== ViewMode.SELECT_PERSON) {
return;
}
try {
await updatePerson({ id: data.person.id, personUpdateDto: { featureFaceAssetId: asset.id } });
notificationController.show({ message: $t('feature_photo_updated'), type: NotificationType.Info });
} catch (error) {
handleError(error, $t('errors.unable_to_set_feature_photo'));
}
await updatePerson({ id: data.person.id, personUpdateDto: { featureFaceAssetId: asset.id } });
notificationController.show({ message: $t('feature_photo_updated'), type: NotificationType.Info });
assetInteractionStore.clearMultiselect();
viewMode = ViewMode.VIEW_ASSETS;
@ -525,7 +528,7 @@
<ImageThumbnail
circle
shadow
url={getPeopleThumbnailUrl(person.id)}
url={getPeopleThumbnailUrl(person)}
altText={person.name}
widthStyle="2rem"
heightStyle="2rem"