mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 02:13:51 -04:00
feat(web): add Exif-Rating (#11580)
* Add Exif-Rating * Integrate star rating as own component * Add e2e tests for rating and validation * Rename component and async handleChangeRating * Display rating can be enabled in app settings * Correct i18n reference Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * Star rating: change from slider to buttons * Star rating for clarity * Design updates. * Renaming and code optimization * chore: clean up * chore: e2e formatting * light mode border and default value --------- Co-authored-by: Christoph Suter <christoph@suter-burri.ch> Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
b1587a5dee
commit
f33dbdfe9a
@ -43,6 +43,7 @@ const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
|
|||||||
const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
|
|
||||||
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
||||||
|
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
|
||||||
|
|
||||||
const readTags = async (bytes: Buffer, filename: string) => {
|
const readTags = async (bytes: Buffer, filename: string) => {
|
||||||
const filepath = join(tempDir, filename);
|
const filepath = join(tempDir, filename);
|
||||||
@ -72,6 +73,7 @@ describe('/asset', () => {
|
|||||||
let user2Assets: AssetMediaResponseDto[];
|
let user2Assets: AssetMediaResponseDto[];
|
||||||
let stackAssets: AssetMediaResponseDto[];
|
let stackAssets: AssetMediaResponseDto[];
|
||||||
let locationAsset: AssetMediaResponseDto;
|
let locationAsset: AssetMediaResponseDto;
|
||||||
|
let ratingAsset: AssetMediaResponseDto;
|
||||||
|
|
||||||
const setupTests = async () => {
|
const setupTests = async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
@ -99,6 +101,16 @@ describe('/asset', () => {
|
|||||||
|
|
||||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: locationAsset.id });
|
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: locationAsset.id });
|
||||||
|
|
||||||
|
// asset rating
|
||||||
|
ratingAsset = await utils.createAsset(admin.accessToken, {
|
||||||
|
assetData: {
|
||||||
|
filename: 'mongolels.jpg',
|
||||||
|
bytes: await readFile(ratingAssetFilepath),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: ratingAsset.id });
|
||||||
|
|
||||||
user1Assets = await Promise.all([
|
user1Assets = await Promise.all([
|
||||||
utils.createAsset(user1.accessToken),
|
utils.createAsset(user1.accessToken),
|
||||||
utils.createAsset(user1.accessToken),
|
utils.createAsset(user1.accessToken),
|
||||||
@ -214,6 +226,22 @@ describe('/asset', () => {
|
|||||||
expect(body).toMatchObject({ id: user1Assets[0].id });
|
expect(body).toMatchObject({ id: user1Assets[0].id });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should get the asset rating', async () => {
|
||||||
|
await utils.waitForWebsocketEvent({
|
||||||
|
event: 'assetUpload',
|
||||||
|
id: ratingAsset.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get(`/assets/${ratingAsset.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: ratingAsset.id,
|
||||||
|
exifInfo: expect.objectContaining({ rating: 3 }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should work with a shared link', async () => {
|
it('should work with a shared link', async () => {
|
||||||
const sharedLink = await utils.createSharedLink(user1.accessToken, {
|
const sharedLink = await utils.createSharedLink(user1.accessToken, {
|
||||||
type: SharedLinkType.Individual,
|
type: SharedLinkType.Individual,
|
||||||
@ -575,6 +603,31 @@ describe('/asset', () => {
|
|||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set the rating', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ rating: 2 });
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: user1Assets[0].id,
|
||||||
|
exifInfo: expect.objectContaining({
|
||||||
|
rating: 2,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid rating', async () => {
|
||||||
|
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
|
.send(test)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should return tagged people', async () => {
|
it('should return tagged people', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/assets/${user1Assets[0].id}`)
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 898069e47f8e3283bf3bbd40b58b56d8fd57dc65
|
Subproject commit 39f25a96f13f743c96cdb7c6d93b031fcb61b83c
|
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -372,6 +372,8 @@ Class | Method | HTTP request | Description
|
|||||||
- [PurchaseResponse](doc//PurchaseResponse.md)
|
- [PurchaseResponse](doc//PurchaseResponse.md)
|
||||||
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
||||||
- [QueueStatusDto](doc//QueueStatusDto.md)
|
- [QueueStatusDto](doc//QueueStatusDto.md)
|
||||||
|
- [RatingResponse](doc//RatingResponse.md)
|
||||||
|
- [RatingUpdate](doc//RatingUpdate.md)
|
||||||
- [ReactionLevel](doc//ReactionLevel.md)
|
- [ReactionLevel](doc//ReactionLevel.md)
|
||||||
- [ReactionType](doc//ReactionType.md)
|
- [ReactionType](doc//ReactionType.md)
|
||||||
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
|
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
|
||||||
|
2
mobile/openapi/lib/api.dart
generated
2
mobile/openapi/lib/api.dart
generated
@ -183,6 +183,8 @@ part 'model/places_response_dto.dart';
|
|||||||
part 'model/purchase_response.dart';
|
part 'model/purchase_response.dart';
|
||||||
part 'model/purchase_update.dart';
|
part 'model/purchase_update.dart';
|
||||||
part 'model/queue_status_dto.dart';
|
part 'model/queue_status_dto.dart';
|
||||||
|
part 'model/rating_response.dart';
|
||||||
|
part 'model/rating_update.dart';
|
||||||
part 'model/reaction_level.dart';
|
part 'model/reaction_level.dart';
|
||||||
part 'model/reaction_type.dart';
|
part 'model/reaction_type.dart';
|
||||||
part 'model/reverse_geocoding_state_response_dto.dart';
|
part 'model/reverse_geocoding_state_response_dto.dart';
|
||||||
|
4
mobile/openapi/lib/api_client.dart
generated
4
mobile/openapi/lib/api_client.dart
generated
@ -424,6 +424,10 @@ class ApiClient {
|
|||||||
return PurchaseUpdate.fromJson(value);
|
return PurchaseUpdate.fromJson(value);
|
||||||
case 'QueueStatusDto':
|
case 'QueueStatusDto':
|
||||||
return QueueStatusDto.fromJson(value);
|
return QueueStatusDto.fromJson(value);
|
||||||
|
case 'RatingResponse':
|
||||||
|
return RatingResponse.fromJson(value);
|
||||||
|
case 'RatingUpdate':
|
||||||
|
return RatingUpdate.fromJson(value);
|
||||||
case 'ReactionLevel':
|
case 'ReactionLevel':
|
||||||
return ReactionLevelTypeTransformer().decode(value);
|
return ReactionLevelTypeTransformer().decode(value);
|
||||||
case 'ReactionType':
|
case 'ReactionType':
|
||||||
|
21
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
21
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
@ -20,6 +20,7 @@ class AssetBulkUpdateDto {
|
|||||||
this.isFavorite,
|
this.isFavorite,
|
||||||
this.latitude,
|
this.latitude,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
|
this.rating,
|
||||||
this.removeParent,
|
this.removeParent,
|
||||||
this.stackParentId,
|
this.stackParentId,
|
||||||
});
|
});
|
||||||
@ -68,6 +69,16 @@ class AssetBulkUpdateDto {
|
|||||||
///
|
///
|
||||||
num? longitude;
|
num? longitude;
|
||||||
|
|
||||||
|
/// Minimum value: 0
|
||||||
|
/// Maximum value: 5
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
num? rating;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// 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
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
@ -93,6 +104,7 @@ class AssetBulkUpdateDto {
|
|||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
other.latitude == latitude &&
|
other.latitude == latitude &&
|
||||||
other.longitude == longitude &&
|
other.longitude == longitude &&
|
||||||
|
other.rating == rating &&
|
||||||
other.removeParent == removeParent &&
|
other.removeParent == removeParent &&
|
||||||
other.stackParentId == stackParentId;
|
other.stackParentId == stackParentId;
|
||||||
|
|
||||||
@ -106,11 +118,12 @@ class AssetBulkUpdateDto {
|
|||||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||||
(latitude == null ? 0 : latitude!.hashCode) +
|
(latitude == null ? 0 : latitude!.hashCode) +
|
||||||
(longitude == null ? 0 : longitude!.hashCode) +
|
(longitude == null ? 0 : longitude!.hashCode) +
|
||||||
|
(rating == null ? 0 : rating!.hashCode) +
|
||||||
(removeParent == null ? 0 : removeParent!.hashCode) +
|
(removeParent == null ? 0 : removeParent!.hashCode) +
|
||||||
(stackParentId == null ? 0 : stackParentId!.hashCode);
|
(stackParentId == null ? 0 : stackParentId!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, removeParent=$removeParent, stackParentId=$stackParentId]';
|
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, removeParent=$removeParent, stackParentId=$stackParentId]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -145,6 +158,11 @@ class AssetBulkUpdateDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'longitude'] = null;
|
// json[r'longitude'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.rating != null) {
|
||||||
|
json[r'rating'] = this.rating;
|
||||||
|
} else {
|
||||||
|
// json[r'rating'] = null;
|
||||||
|
}
|
||||||
if (this.removeParent != null) {
|
if (this.removeParent != null) {
|
||||||
json[r'removeParent'] = this.removeParent;
|
json[r'removeParent'] = this.removeParent;
|
||||||
} else {
|
} else {
|
||||||
@ -175,6 +193,7 @@ class AssetBulkUpdateDto {
|
|||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||||
latitude: num.parse('${json[r'latitude']}'),
|
latitude: num.parse('${json[r'latitude']}'),
|
||||||
longitude: num.parse('${json[r'longitude']}'),
|
longitude: num.parse('${json[r'longitude']}'),
|
||||||
|
rating: num.parse('${json[r'rating']}'),
|
||||||
removeParent: mapValueOfType<bool>(json, r'removeParent'),
|
removeParent: mapValueOfType<bool>(json, r'removeParent'),
|
||||||
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
|
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
|
||||||
);
|
);
|
||||||
|
15
mobile/openapi/lib/model/exif_response_dto.dart
generated
15
mobile/openapi/lib/model/exif_response_dto.dart
generated
@ -32,6 +32,7 @@ class ExifResponseDto {
|
|||||||
this.modifyDate,
|
this.modifyDate,
|
||||||
this.orientation,
|
this.orientation,
|
||||||
this.projectionType,
|
this.projectionType,
|
||||||
|
this.rating,
|
||||||
this.state,
|
this.state,
|
||||||
this.timeZone,
|
this.timeZone,
|
||||||
});
|
});
|
||||||
@ -74,6 +75,8 @@ class ExifResponseDto {
|
|||||||
|
|
||||||
String? projectionType;
|
String? projectionType;
|
||||||
|
|
||||||
|
num? rating;
|
||||||
|
|
||||||
String? state;
|
String? state;
|
||||||
|
|
||||||
String? timeZone;
|
String? timeZone;
|
||||||
@ -99,6 +102,7 @@ class ExifResponseDto {
|
|||||||
other.modifyDate == modifyDate &&
|
other.modifyDate == modifyDate &&
|
||||||
other.orientation == orientation &&
|
other.orientation == orientation &&
|
||||||
other.projectionType == projectionType &&
|
other.projectionType == projectionType &&
|
||||||
|
other.rating == rating &&
|
||||||
other.state == state &&
|
other.state == state &&
|
||||||
other.timeZone == timeZone;
|
other.timeZone == timeZone;
|
||||||
|
|
||||||
@ -124,11 +128,12 @@ class ExifResponseDto {
|
|||||||
(modifyDate == null ? 0 : modifyDate!.hashCode) +
|
(modifyDate == null ? 0 : modifyDate!.hashCode) +
|
||||||
(orientation == null ? 0 : orientation!.hashCode) +
|
(orientation == null ? 0 : orientation!.hashCode) +
|
||||||
(projectionType == null ? 0 : projectionType!.hashCode) +
|
(projectionType == null ? 0 : projectionType!.hashCode) +
|
||||||
|
(rating == null ? 0 : rating!.hashCode) +
|
||||||
(state == null ? 0 : state!.hashCode) +
|
(state == null ? 0 : state!.hashCode) +
|
||||||
(timeZone == null ? 0 : timeZone!.hashCode);
|
(timeZone == null ? 0 : timeZone!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ExifResponseDto[city=$city, country=$country, dateTimeOriginal=$dateTimeOriginal, description=$description, exifImageHeight=$exifImageHeight, exifImageWidth=$exifImageWidth, exposureTime=$exposureTime, fNumber=$fNumber, fileSizeInByte=$fileSizeInByte, focalLength=$focalLength, iso=$iso, latitude=$latitude, lensModel=$lensModel, longitude=$longitude, make=$make, model=$model, modifyDate=$modifyDate, orientation=$orientation, projectionType=$projectionType, state=$state, timeZone=$timeZone]';
|
String toString() => 'ExifResponseDto[city=$city, country=$country, dateTimeOriginal=$dateTimeOriginal, description=$description, exifImageHeight=$exifImageHeight, exifImageWidth=$exifImageWidth, exposureTime=$exposureTime, fNumber=$fNumber, fileSizeInByte=$fileSizeInByte, focalLength=$focalLength, iso=$iso, latitude=$latitude, lensModel=$lensModel, longitude=$longitude, make=$make, model=$model, modifyDate=$modifyDate, orientation=$orientation, projectionType=$projectionType, rating=$rating, state=$state, timeZone=$timeZone]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -227,6 +232,11 @@ class ExifResponseDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'projectionType'] = null;
|
// json[r'projectionType'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.rating != null) {
|
||||||
|
json[r'rating'] = this.rating;
|
||||||
|
} else {
|
||||||
|
// json[r'rating'] = null;
|
||||||
|
}
|
||||||
if (this.state != null) {
|
if (this.state != null) {
|
||||||
json[r'state'] = this.state;
|
json[r'state'] = this.state;
|
||||||
} else {
|
} else {
|
||||||
@ -281,6 +291,9 @@ class ExifResponseDto {
|
|||||||
modifyDate: mapDateTime(json, r'modifyDate', r''),
|
modifyDate: mapDateTime(json, r'modifyDate', r''),
|
||||||
orientation: mapValueOfType<String>(json, r'orientation'),
|
orientation: mapValueOfType<String>(json, r'orientation'),
|
||||||
projectionType: mapValueOfType<String>(json, r'projectionType'),
|
projectionType: mapValueOfType<String>(json, r'projectionType'),
|
||||||
|
rating: json[r'rating'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'rating']}'),
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
||||||
);
|
);
|
||||||
|
98
mobile/openapi/lib/model/rating_response.dart
generated
Normal file
98
mobile/openapi/lib/model/rating_response.dart
generated
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class RatingResponse {
|
||||||
|
/// Returns a new [RatingResponse] instance.
|
||||||
|
RatingResponse({
|
||||||
|
required this.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is RatingResponse &&
|
||||||
|
other.enabled == enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(enabled.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'RatingResponse[enabled=$enabled]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'enabled'] = this.enabled;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [RatingResponse] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static RatingResponse? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return RatingResponse(
|
||||||
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<RatingResponse> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <RatingResponse>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = RatingResponse.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, RatingResponse> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, RatingResponse>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = RatingResponse.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of RatingResponse-objects as value to a dart map
|
||||||
|
static Map<String, List<RatingResponse>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<RatingResponse>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = RatingResponse.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'enabled',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
107
mobile/openapi/lib/model/rating_update.dart
generated
Normal file
107
mobile/openapi/lib/model/rating_update.dart
generated
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class RatingUpdate {
|
||||||
|
/// Returns a new [RatingUpdate] instance.
|
||||||
|
RatingUpdate({
|
||||||
|
this.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
bool? enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is RatingUpdate &&
|
||||||
|
other.enabled == enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(enabled == null ? 0 : enabled!.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'RatingUpdate[enabled=$enabled]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
if (this.enabled != null) {
|
||||||
|
json[r'enabled'] = this.enabled;
|
||||||
|
} else {
|
||||||
|
// json[r'enabled'] = null;
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [RatingUpdate] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static RatingUpdate? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return RatingUpdate(
|
||||||
|
enabled: mapValueOfType<bool>(json, r'enabled'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<RatingUpdate> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <RatingUpdate>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = RatingUpdate.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, RatingUpdate> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, RatingUpdate>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = RatingUpdate.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of RatingUpdate-objects as value to a dart map
|
||||||
|
static Map<String, List<RatingUpdate>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<RatingUpdate>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = RatingUpdate.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
25
mobile/openapi/lib/model/update_asset_dto.dart
generated
25
mobile/openapi/lib/model/update_asset_dto.dart
generated
@ -19,6 +19,7 @@ class UpdateAssetDto {
|
|||||||
this.isFavorite,
|
this.isFavorite,
|
||||||
this.latitude,
|
this.latitude,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
|
this.rating,
|
||||||
});
|
});
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -69,6 +70,16 @@ class UpdateAssetDto {
|
|||||||
///
|
///
|
||||||
num? longitude;
|
num? longitude;
|
||||||
|
|
||||||
|
/// Minimum value: 0
|
||||||
|
/// Maximum value: 5
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
num? rating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
|
||||||
other.dateTimeOriginal == dateTimeOriginal &&
|
other.dateTimeOriginal == dateTimeOriginal &&
|
||||||
@ -76,7 +87,8 @@ class UpdateAssetDto {
|
|||||||
other.isArchived == isArchived &&
|
other.isArchived == isArchived &&
|
||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
other.latitude == latitude &&
|
other.latitude == latitude &&
|
||||||
other.longitude == longitude;
|
other.longitude == longitude &&
|
||||||
|
other.rating == rating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@ -86,10 +98,11 @@ class UpdateAssetDto {
|
|||||||
(isArchived == null ? 0 : isArchived!.hashCode) +
|
(isArchived == null ? 0 : isArchived!.hashCode) +
|
||||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||||
(latitude == null ? 0 : latitude!.hashCode) +
|
(latitude == null ? 0 : latitude!.hashCode) +
|
||||||
(longitude == null ? 0 : longitude!.hashCode);
|
(longitude == null ? 0 : longitude!.hashCode) +
|
||||||
|
(rating == null ? 0 : rating!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude]';
|
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -123,6 +136,11 @@ class UpdateAssetDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'longitude'] = null;
|
// json[r'longitude'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.rating != null) {
|
||||||
|
json[r'rating'] = this.rating;
|
||||||
|
} else {
|
||||||
|
// json[r'rating'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,6 +158,7 @@ class UpdateAssetDto {
|
|||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||||
latitude: num.parse('${json[r'latitude']}'),
|
latitude: num.parse('${json[r'latitude']}'),
|
||||||
longitude: num.parse('${json[r'longitude']}'),
|
longitude: num.parse('${json[r'longitude']}'),
|
||||||
|
rating: num.parse('${json[r'rating']}'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -18,6 +18,7 @@ class UserPreferencesResponseDto {
|
|||||||
required this.emailNotifications,
|
required this.emailNotifications,
|
||||||
required this.memories,
|
required this.memories,
|
||||||
required this.purchase,
|
required this.purchase,
|
||||||
|
required this.rating,
|
||||||
});
|
});
|
||||||
|
|
||||||
AvatarResponse avatar;
|
AvatarResponse avatar;
|
||||||
@ -30,13 +31,16 @@ class UserPreferencesResponseDto {
|
|||||||
|
|
||||||
PurchaseResponse purchase;
|
PurchaseResponse purchase;
|
||||||
|
|
||||||
|
RatingResponse rating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto &&
|
||||||
other.avatar == avatar &&
|
other.avatar == avatar &&
|
||||||
other.download == download &&
|
other.download == download &&
|
||||||
other.emailNotifications == emailNotifications &&
|
other.emailNotifications == emailNotifications &&
|
||||||
other.memories == memories &&
|
other.memories == memories &&
|
||||||
other.purchase == purchase;
|
other.purchase == purchase &&
|
||||||
|
other.rating == rating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@ -45,10 +49,11 @@ class UserPreferencesResponseDto {
|
|||||||
(download.hashCode) +
|
(download.hashCode) +
|
||||||
(emailNotifications.hashCode) +
|
(emailNotifications.hashCode) +
|
||||||
(memories.hashCode) +
|
(memories.hashCode) +
|
||||||
(purchase.hashCode);
|
(purchase.hashCode) +
|
||||||
|
(rating.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase]';
|
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase, rating=$rating]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -57,6 +62,7 @@ class UserPreferencesResponseDto {
|
|||||||
json[r'emailNotifications'] = this.emailNotifications;
|
json[r'emailNotifications'] = this.emailNotifications;
|
||||||
json[r'memories'] = this.memories;
|
json[r'memories'] = this.memories;
|
||||||
json[r'purchase'] = this.purchase;
|
json[r'purchase'] = this.purchase;
|
||||||
|
json[r'rating'] = this.rating;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +79,7 @@ class UserPreferencesResponseDto {
|
|||||||
emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!,
|
emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!,
|
||||||
memories: MemoryResponse.fromJson(json[r'memories'])!,
|
memories: MemoryResponse.fromJson(json[r'memories'])!,
|
||||||
purchase: PurchaseResponse.fromJson(json[r'purchase'])!,
|
purchase: PurchaseResponse.fromJson(json[r'purchase'])!,
|
||||||
|
rating: RatingResponse.fromJson(json[r'rating'])!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -125,6 +132,7 @@ class UserPreferencesResponseDto {
|
|||||||
'emailNotifications',
|
'emailNotifications',
|
||||||
'memories',
|
'memories',
|
||||||
'purchase',
|
'purchase',
|
||||||
|
'rating',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ class UserPreferencesUpdateDto {
|
|||||||
this.emailNotifications,
|
this.emailNotifications,
|
||||||
this.memories,
|
this.memories,
|
||||||
this.purchase,
|
this.purchase,
|
||||||
|
this.rating,
|
||||||
});
|
});
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -60,13 +61,22 @@ class UserPreferencesUpdateDto {
|
|||||||
///
|
///
|
||||||
PurchaseUpdate? purchase;
|
PurchaseUpdate? purchase;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
RatingUpdate? rating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto &&
|
||||||
other.avatar == avatar &&
|
other.avatar == avatar &&
|
||||||
other.download == download &&
|
other.download == download &&
|
||||||
other.emailNotifications == emailNotifications &&
|
other.emailNotifications == emailNotifications &&
|
||||||
other.memories == memories &&
|
other.memories == memories &&
|
||||||
other.purchase == purchase;
|
other.purchase == purchase &&
|
||||||
|
other.rating == rating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@ -75,10 +85,11 @@ class UserPreferencesUpdateDto {
|
|||||||
(download == null ? 0 : download!.hashCode) +
|
(download == null ? 0 : download!.hashCode) +
|
||||||
(emailNotifications == null ? 0 : emailNotifications!.hashCode) +
|
(emailNotifications == null ? 0 : emailNotifications!.hashCode) +
|
||||||
(memories == null ? 0 : memories!.hashCode) +
|
(memories == null ? 0 : memories!.hashCode) +
|
||||||
(purchase == null ? 0 : purchase!.hashCode);
|
(purchase == null ? 0 : purchase!.hashCode) +
|
||||||
|
(rating == null ? 0 : rating!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase]';
|
String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase, rating=$rating]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -107,6 +118,11 @@ class UserPreferencesUpdateDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'purchase'] = null;
|
// json[r'purchase'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.rating != null) {
|
||||||
|
json[r'rating'] = this.rating;
|
||||||
|
} else {
|
||||||
|
// json[r'rating'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +139,7 @@ class UserPreferencesUpdateDto {
|
|||||||
emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']),
|
emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']),
|
||||||
memories: MemoryUpdate.fromJson(json[r'memories']),
|
memories: MemoryUpdate.fromJson(json[r'memories']),
|
||||||
purchase: PurchaseUpdate.fromJson(json[r'purchase']),
|
purchase: PurchaseUpdate.fromJson(json[r'purchase']),
|
||||||
|
rating: RatingUpdate.fromJson(json[r'rating']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -7550,6 +7550,11 @@
|
|||||||
"longitude": {
|
"longitude": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
"rating": {
|
||||||
|
"maximum": 5,
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
"removeParent": {
|
"removeParent": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@ -8702,6 +8707,11 @@
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"rating": {
|
||||||
|
"default": null,
|
||||||
|
"nullable": true,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"default": null,
|
"default": null,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
@ -9905,6 +9915,25 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"RatingResponse": {
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"enabled"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"RatingUpdate": {
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ReactionLevel": {
|
"ReactionLevel": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"album",
|
"album",
|
||||||
@ -11565,6 +11594,11 @@
|
|||||||
},
|
},
|
||||||
"longitude": {
|
"longitude": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"maximum": 5,
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@ -11865,6 +11899,9 @@
|
|||||||
},
|
},
|
||||||
"purchase": {
|
"purchase": {
|
||||||
"$ref": "#/components/schemas/PurchaseResponse"
|
"$ref": "#/components/schemas/PurchaseResponse"
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"$ref": "#/components/schemas/RatingResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -11872,7 +11909,8 @@
|
|||||||
"download",
|
"download",
|
||||||
"emailNotifications",
|
"emailNotifications",
|
||||||
"memories",
|
"memories",
|
||||||
"purchase"
|
"purchase",
|
||||||
|
"rating"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@ -11892,6 +11930,9 @@
|
|||||||
},
|
},
|
||||||
"purchase": {
|
"purchase": {
|
||||||
"$ref": "#/components/schemas/PurchaseUpdate"
|
"$ref": "#/components/schemas/PurchaseUpdate"
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"$ref": "#/components/schemas/RatingUpdate"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
@ -99,12 +99,16 @@ export type PurchaseResponse = {
|
|||||||
hideBuyButtonUntil: string;
|
hideBuyButtonUntil: string;
|
||||||
showSupportBadge: boolean;
|
showSupportBadge: boolean;
|
||||||
};
|
};
|
||||||
|
export type RatingResponse = {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
export type UserPreferencesResponseDto = {
|
export type UserPreferencesResponseDto = {
|
||||||
avatar: AvatarResponse;
|
avatar: AvatarResponse;
|
||||||
download: DownloadResponse;
|
download: DownloadResponse;
|
||||||
emailNotifications: EmailNotificationsResponse;
|
emailNotifications: EmailNotificationsResponse;
|
||||||
memories: MemoryResponse;
|
memories: MemoryResponse;
|
||||||
purchase: PurchaseResponse;
|
purchase: PurchaseResponse;
|
||||||
|
rating: RatingResponse;
|
||||||
};
|
};
|
||||||
export type AvatarUpdate = {
|
export type AvatarUpdate = {
|
||||||
color?: UserAvatarColor;
|
color?: UserAvatarColor;
|
||||||
@ -124,12 +128,16 @@ export type PurchaseUpdate = {
|
|||||||
hideBuyButtonUntil?: string;
|
hideBuyButtonUntil?: string;
|
||||||
showSupportBadge?: boolean;
|
showSupportBadge?: boolean;
|
||||||
};
|
};
|
||||||
|
export type RatingUpdate = {
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
export type UserPreferencesUpdateDto = {
|
export type UserPreferencesUpdateDto = {
|
||||||
avatar?: AvatarUpdate;
|
avatar?: AvatarUpdate;
|
||||||
download?: DownloadUpdate;
|
download?: DownloadUpdate;
|
||||||
emailNotifications?: EmailNotificationsUpdate;
|
emailNotifications?: EmailNotificationsUpdate;
|
||||||
memories?: MemoryUpdate;
|
memories?: MemoryUpdate;
|
||||||
purchase?: PurchaseUpdate;
|
purchase?: PurchaseUpdate;
|
||||||
|
rating?: RatingUpdate;
|
||||||
};
|
};
|
||||||
export type AlbumUserResponseDto = {
|
export type AlbumUserResponseDto = {
|
||||||
role: AlbumUserRole;
|
role: AlbumUserRole;
|
||||||
@ -155,6 +163,7 @@ export type ExifResponseDto = {
|
|||||||
modifyDate?: string | null;
|
modifyDate?: string | null;
|
||||||
orientation?: string | null;
|
orientation?: string | null;
|
||||||
projectionType?: string | null;
|
projectionType?: string | null;
|
||||||
|
rating?: number | null;
|
||||||
state?: string | null;
|
state?: string | null;
|
||||||
timeZone?: string | null;
|
timeZone?: string | null;
|
||||||
};
|
};
|
||||||
@ -330,6 +339,7 @@ export type AssetBulkUpdateDto = {
|
|||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
|
rating?: number;
|
||||||
removeParent?: boolean;
|
removeParent?: boolean;
|
||||||
stackParentId?: string;
|
stackParentId?: string;
|
||||||
};
|
};
|
||||||
@ -381,6 +391,7 @@ export type UpdateAssetDto = {
|
|||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
|
rating?: number;
|
||||||
};
|
};
|
||||||
export type AssetMediaReplaceDto = {
|
export type AssetMediaReplaceDto = {
|
||||||
assetData: Blob;
|
assetData: Blob;
|
||||||
|
@ -9,6 +9,8 @@ import {
|
|||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsPositive,
|
IsPositive,
|
||||||
IsString,
|
IsString,
|
||||||
|
Max,
|
||||||
|
Min,
|
||||||
ValidateIf,
|
ValidateIf,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
@ -46,6 +48,12 @@ export class UpdateAssetBase {
|
|||||||
@IsLongitude()
|
@IsLongitude()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@IsInt()
|
||||||
|
@Max(5)
|
||||||
|
@Min(0)
|
||||||
|
rating?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AssetBulkUpdateDto extends UpdateAssetBase {
|
export class AssetBulkUpdateDto extends UpdateAssetBase {
|
||||||
|
@ -25,6 +25,7 @@ export class ExifResponseDto {
|
|||||||
country?: string | null = null;
|
country?: string | null = null;
|
||||||
description?: string | null = null;
|
description?: string | null = null;
|
||||||
projectionType?: string | null = null;
|
projectionType?: string | null = null;
|
||||||
|
rating?: number | null = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapExif(entity: ExifEntity): ExifResponseDto {
|
export function mapExif(entity: ExifEntity): ExifResponseDto {
|
||||||
@ -50,6 +51,7 @@ export function mapExif(entity: ExifEntity): ExifResponseDto {
|
|||||||
country: entity.country,
|
country: entity.country,
|
||||||
description: entity.description,
|
description: entity.description,
|
||||||
projectionType: entity.projectionType,
|
projectionType: entity.projectionType,
|
||||||
|
rating: entity.rating,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,5 +64,6 @@ export function mapSanitizedExif(entity: ExifEntity): ExifResponseDto {
|
|||||||
projectionType: entity.projectionType,
|
projectionType: entity.projectionType,
|
||||||
exifImageWidth: entity.exifImageWidth,
|
exifImageWidth: entity.exifImageWidth,
|
||||||
exifImageHeight: entity.exifImageHeight,
|
exifImageHeight: entity.exifImageHeight,
|
||||||
|
rating: entity.rating,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,11 @@ class MemoryUpdate {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RatingUpdate {
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
class EmailNotificationsUpdate {
|
class EmailNotificationsUpdate {
|
||||||
@ValidateBoolean({ optional: true })
|
@ValidateBoolean({ optional: true })
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@ -45,6 +50,11 @@ class PurchaseUpdate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UserPreferencesUpdateDto {
|
export class UserPreferencesUpdateDto {
|
||||||
|
@Optional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => RatingUpdate)
|
||||||
|
rating?: RatingUpdate;
|
||||||
|
|
||||||
@Optional()
|
@Optional()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => AvatarUpdate)
|
@Type(() => AvatarUpdate)
|
||||||
@ -76,6 +86,10 @@ class AvatarResponse {
|
|||||||
color!: UserAvatarColor;
|
color!: UserAvatarColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RatingResponse {
|
||||||
|
enabled!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
class MemoryResponse {
|
class MemoryResponse {
|
||||||
enabled!: boolean;
|
enabled!: boolean;
|
||||||
}
|
}
|
||||||
@ -97,6 +111,7 @@ class PurchaseResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UserPreferencesResponseDto implements UserPreferences {
|
export class UserPreferencesResponseDto implements UserPreferences {
|
||||||
|
rating!: RatingResponse;
|
||||||
memories!: MemoryResponse;
|
memories!: MemoryResponse;
|
||||||
avatar!: AvatarResponse;
|
avatar!: AvatarResponse;
|
||||||
emailNotifications!: EmailNotificationsResponse;
|
emailNotifications!: EmailNotificationsResponse;
|
||||||
|
@ -95,6 +95,9 @@ export class ExifEntity {
|
|||||||
@Column({ type: 'integer', nullable: true })
|
@Column({ type: 'integer', nullable: true })
|
||||||
bitsPerSample!: number | null;
|
bitsPerSample!: number | null;
|
||||||
|
|
||||||
|
@Column({ type: 'integer', nullable: true })
|
||||||
|
rating!: number | null;
|
||||||
|
|
||||||
/* Video info */
|
/* Video info */
|
||||||
@Column({ type: 'float8', nullable: true })
|
@Column({ type: 'float8', nullable: true })
|
||||||
fps?: number | null;
|
fps?: number | null;
|
||||||
|
@ -31,6 +31,9 @@ export enum UserAvatarColor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
|
rating: {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
memories: {
|
memories: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
@ -58,6 +61,9 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
rating: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
memories: {
|
memories: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
@ -147,6 +147,7 @@ export interface ISidecarWriteJob extends IEntityJob {
|
|||||||
dateTimeOriginal?: string;
|
dateTimeOriginal?: string;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
|
rating?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDeferrableJob extends IEntityJob {
|
export interface IDeferrableJob extends IEntityJob {
|
||||||
|
14
server/src/migrations/1722753178937-AddExifRating.ts
Normal file
14
server/src/migrations/1722753178937-AddExifRating.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddRating1722753178937 implements MigrationInterface {
|
||||||
|
name = 'AddRating1722753178937'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "exif" ADD "rating" integer`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "rating"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -58,6 +58,7 @@ SELECT
|
|||||||
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
||||||
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
||||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||||
|
"exifInfo"."rating" AS "exifInfo_rating",
|
||||||
"exifInfo"."fps" AS "exifInfo_fps"
|
"exifInfo"."fps" AS "exifInfo_fps"
|
||||||
FROM
|
FROM
|
||||||
"assets" "entity"
|
"assets" "entity"
|
||||||
@ -177,6 +178,7 @@ SELECT
|
|||||||
"AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription",
|
"AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription",
|
||||||
"AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace",
|
"AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace",
|
||||||
"AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample",
|
"AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample",
|
||||||
|
"AssetEntity__AssetEntity_exifInfo"."rating" AS "AssetEntity__AssetEntity_exifInfo_rating",
|
||||||
"AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps",
|
"AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps",
|
||||||
"AssetEntity__AssetEntity_smartInfo"."assetId" AS "AssetEntity__AssetEntity_smartInfo_assetId",
|
"AssetEntity__AssetEntity_smartInfo"."assetId" AS "AssetEntity__AssetEntity_smartInfo_assetId",
|
||||||
"AssetEntity__AssetEntity_smartInfo"."tags" AS "AssetEntity__AssetEntity_smartInfo_tags",
|
"AssetEntity__AssetEntity_smartInfo"."tags" AS "AssetEntity__AssetEntity_smartInfo_tags",
|
||||||
@ -628,6 +630,7 @@ SELECT
|
|||||||
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
||||||
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
||||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||||
|
"exifInfo"."rating" AS "exifInfo_rating",
|
||||||
"exifInfo"."fps" AS "exifInfo_fps",
|
"exifInfo"."fps" AS "exifInfo_fps",
|
||||||
"stack"."id" AS "stack_id",
|
"stack"."id" AS "stack_id",
|
||||||
"stack"."ownerId" AS "stack_ownerId",
|
"stack"."ownerId" AS "stack_ownerId",
|
||||||
@ -769,6 +772,7 @@ SELECT
|
|||||||
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
||||||
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
||||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||||
|
"exifInfo"."rating" AS "exifInfo_rating",
|
||||||
"exifInfo"."fps" AS "exifInfo_fps",
|
"exifInfo"."fps" AS "exifInfo_fps",
|
||||||
"stack"."id" AS "stack_id",
|
"stack"."id" AS "stack_id",
|
||||||
"stack"."ownerId" AS "stack_ownerId",
|
"stack"."ownerId" AS "stack_ownerId",
|
||||||
@ -886,6 +890,7 @@ SELECT
|
|||||||
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
||||||
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
||||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||||
|
"exifInfo"."rating" AS "exifInfo_rating",
|
||||||
"exifInfo"."fps" AS "exifInfo_fps",
|
"exifInfo"."fps" AS "exifInfo_fps",
|
||||||
"stack"."id" AS "stack_id",
|
"stack"."id" AS "stack_id",
|
||||||
"stack"."ownerId" AS "stack_ownerId",
|
"stack"."ownerId" AS "stack_ownerId",
|
||||||
@ -1053,6 +1058,7 @@ SELECT
|
|||||||
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
||||||
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
||||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||||
|
"exifInfo"."rating" AS "exifInfo_rating",
|
||||||
"exifInfo"."fps" AS "exifInfo_fps",
|
"exifInfo"."fps" AS "exifInfo_fps",
|
||||||
"stack"."id" AS "stack_id",
|
"stack"."id" AS "stack_id",
|
||||||
"stack"."ownerId" AS "stack_ownerId",
|
"stack"."ownerId" AS "stack_ownerId",
|
||||||
@ -1129,6 +1135,7 @@ SELECT
|
|||||||
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
||||||
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
||||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||||
|
"exifInfo"."rating" AS "exifInfo_rating",
|
||||||
"exifInfo"."fps" AS "exifInfo_fps",
|
"exifInfo"."fps" AS "exifInfo_fps",
|
||||||
"stack"."id" AS "stack_id",
|
"stack"."id" AS "stack_id",
|
||||||
"stack"."ownerId" AS "stack_ownerId",
|
"stack"."ownerId" AS "stack_ownerId",
|
||||||
|
@ -322,6 +322,7 @@ FROM
|
|||||||
"AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription",
|
"AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription",
|
||||||
"AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace",
|
"AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace",
|
||||||
"AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample",
|
"AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample",
|
||||||
|
"AssetEntity__AssetEntity_exifInfo"."rating" AS "AssetEntity__AssetEntity_exifInfo_rating",
|
||||||
"AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps"
|
"AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps"
|
||||||
FROM
|
FROM
|
||||||
"assets" "AssetEntity"
|
"assets" "AssetEntity"
|
||||||
|
@ -402,6 +402,7 @@ SELECT
|
|||||||
"exif"."profileDescription" AS "exif_profileDescription",
|
"exif"."profileDescription" AS "exif_profileDescription",
|
||||||
"exif"."colorspace" AS "exif_colorspace",
|
"exif"."colorspace" AS "exif_colorspace",
|
||||||
"exif"."bitsPerSample" AS "exif_bitsPerSample",
|
"exif"."bitsPerSample" AS "exif_bitsPerSample",
|
||||||
|
"exif"."rating" AS "exif_rating",
|
||||||
"exif"."fps" AS "exif_fps"
|
"exif"."fps" AS "exif_fps"
|
||||||
FROM
|
FROM
|
||||||
"assets" "asset"
|
"assets" "asset"
|
||||||
|
@ -77,6 +77,7 @@ FROM
|
|||||||
"9b1d35b344d838023994a3233afd6ffe098be6d8"."profileDescription" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_profileDescription",
|
"9b1d35b344d838023994a3233afd6ffe098be6d8"."profileDescription" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_profileDescription",
|
||||||
"9b1d35b344d838023994a3233afd6ffe098be6d8"."colorspace" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_colorspace",
|
"9b1d35b344d838023994a3233afd6ffe098be6d8"."colorspace" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_colorspace",
|
||||||
"9b1d35b344d838023994a3233afd6ffe098be6d8"."bitsPerSample" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_bitsPerSample",
|
"9b1d35b344d838023994a3233afd6ffe098be6d8"."bitsPerSample" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_bitsPerSample",
|
||||||
|
"9b1d35b344d838023994a3233afd6ffe098be6d8"."rating" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_rating",
|
||||||
"9b1d35b344d838023994a3233afd6ffe098be6d8"."fps" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_fps",
|
"9b1d35b344d838023994a3233afd6ffe098be6d8"."fps" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_fps",
|
||||||
"SharedLinkEntity__SharedLinkEntity_album"."id" AS "SharedLinkEntity__SharedLinkEntity_album_id",
|
"SharedLinkEntity__SharedLinkEntity_album"."id" AS "SharedLinkEntity__SharedLinkEntity_album_id",
|
||||||
"SharedLinkEntity__SharedLinkEntity_album"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_album_ownerId",
|
"SharedLinkEntity__SharedLinkEntity_album"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_album_ownerId",
|
||||||
@ -144,6 +145,7 @@ FROM
|
|||||||
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."profileDescription" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_profileDescription",
|
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."profileDescription" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_profileDescription",
|
||||||
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."colorspace" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_colorspace",
|
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."colorspace" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_colorspace",
|
||||||
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."bitsPerSample" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_bitsPerSample",
|
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."bitsPerSample" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_bitsPerSample",
|
||||||
|
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."rating" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_rating",
|
||||||
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."fps" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_fps",
|
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."fps" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_fps",
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id",
|
||||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name",
|
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name",
|
||||||
|
@ -228,6 +228,13 @@ describe(AssetService.name, () => {
|
|||||||
await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
|
await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
|
||||||
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
|
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update the exif rating', async () => {
|
||||||
|
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
|
assetMock.getById.mockResolvedValue(assetStub.image);
|
||||||
|
await sut.update(authStub.admin, 'asset-1', { rating: 3 });
|
||||||
|
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', rating: 3 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('updateAll', () => {
|
describe('updateAll', () => {
|
||||||
|
@ -158,8 +158,8 @@ export class AssetService {
|
|||||||
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||||
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id);
|
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id);
|
||||||
|
|
||||||
const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
|
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
||||||
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
|
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
|
||||||
|
|
||||||
await this.assetRepository.update({ id, ...rest });
|
await this.assetRepository.update({ id, ...rest });
|
||||||
const asset = await this.assetRepository.getById(id, {
|
const asset = await this.assetRepository.getById(id, {
|
||||||
@ -405,8 +405,8 @@ export class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async updateMetadata(dto: ISidecarWriteJob) {
|
private async updateMetadata(dto: ISidecarWriteJob) {
|
||||||
const { id, description, dateTimeOriginal, latitude, longitude } = dto;
|
const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto;
|
||||||
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude }, _.isUndefined);
|
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined);
|
||||||
if (Object.keys(writes).length > 0) {
|
if (Object.keys(writes).length > 0) {
|
||||||
await this.assetRepository.upsertExif({ assetId: id, ...writes });
|
await this.assetRepository.upsertExif({ assetId: id, ...writes });
|
||||||
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } });
|
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } });
|
||||||
|
@ -606,6 +606,7 @@ describe(MetadataService.name, () => {
|
|||||||
ProfileDescription: 'extensive description',
|
ProfileDescription: 'extensive description',
|
||||||
ProjectionType: 'equirectangular',
|
ProjectionType: 'equirectangular',
|
||||||
tz: '+02:00',
|
tz: '+02:00',
|
||||||
|
Rating: 3,
|
||||||
};
|
};
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
metadataMock.readTags.mockResolvedValue(tags);
|
metadataMock.readTags.mockResolvedValue(tags);
|
||||||
@ -638,6 +639,7 @@ describe(MetadataService.name, () => {
|
|||||||
profileDescription: tags.ProfileDescription,
|
profileDescription: tags.ProfileDescription,
|
||||||
projectionType: 'EQUIRECTANGULAR',
|
projectionType: 'EQUIRECTANGULAR',
|
||||||
timeZone: tags.tz,
|
timeZone: tags.tz,
|
||||||
|
rating: tags.Rating,
|
||||||
});
|
});
|
||||||
expect(assetMock.update).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
|
@ -273,7 +273,7 @@ export class MetadataService implements OnEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSidecarWrite(job: ISidecarWriteJob): Promise<JobStatus> {
|
async handleSidecarWrite(job: ISidecarWriteJob): Promise<JobStatus> {
|
||||||
const { id, description, dateTimeOriginal, latitude, longitude } = job;
|
const { id, description, dateTimeOriginal, latitude, longitude, rating } = job;
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
@ -287,6 +287,7 @@ export class MetadataService implements OnEvents {
|
|||||||
DateTimeOriginal: dateTimeOriginal,
|
DateTimeOriginal: dateTimeOriginal,
|
||||||
GPSLatitude: latitude,
|
GPSLatitude: latitude,
|
||||||
GPSLongitude: longitude,
|
GPSLongitude: longitude,
|
||||||
|
Rating: rating,
|
||||||
},
|
},
|
||||||
_.isUndefined,
|
_.isUndefined,
|
||||||
);
|
);
|
||||||
@ -503,6 +504,7 @@ export class MetadataService implements OnEvents {
|
|||||||
profileDescription: tags.ProfileDescription || null,
|
profileDescription: tags.ProfileDescription || null,
|
||||||
projectionType: tags.ProjectionType ? String(tags.ProjectionType).toUpperCase() : null,
|
projectionType: tags.ProjectionType ? String(tags.ProjectionType).toUpperCase() : null,
|
||||||
timeZone: tags.tz ?? null,
|
timeZone: tags.tz ?? null,
|
||||||
|
rating: tags.Rating ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (exifData.latitude === 0 && exifData.longitude === 0) {
|
if (exifData.latitude === 0 && exifData.longitude === 0) {
|
||||||
|
1
server/test/fixtures/shared-link.stub.ts
vendored
1
server/test/fixtures/shared-link.stub.ts
vendored
@ -253,6 +253,7 @@ export const sharedLinkStub = {
|
|||||||
bitsPerSample: 8,
|
bitsPerSample: 8,
|
||||||
colorspace: 'sRGB',
|
colorspace: 'sRGB',
|
||||||
autoStackId: null,
|
autoStackId: null,
|
||||||
|
rating: 3,
|
||||||
},
|
},
|
||||||
tags: [],
|
tags: [],
|
||||||
sharedLinks: [],
|
sharedLinks: [],
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { updateAsset, type AssetResponseDto } from '@immich/sdk';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import StarRating from '$lib/components/shared-components/star-rating.svelte';
|
||||||
|
import { handlePromiseError, isSharedLink } from '$lib/utils';
|
||||||
|
import { preferences } from '$lib/stores/user.store';
|
||||||
|
|
||||||
|
export let asset: AssetResponseDto;
|
||||||
|
export let isOwner: boolean;
|
||||||
|
|
||||||
|
$: rating = asset.exifInfo?.rating || 0;
|
||||||
|
|
||||||
|
const handleChangeRating = async (rating: number) => {
|
||||||
|
try {
|
||||||
|
await updateAsset({ id: asset.id, updateAssetDto: { rating } });
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.cant_apply_changes'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !isSharedLink() && $preferences?.rating?.enabled}
|
||||||
|
<section class="relative flex px-4 pt-2">
|
||||||
|
<StarRating {rating} readOnly={!isOwner} onRating={(rating) => handlePromiseError(handleChangeRating(rating))} />
|
||||||
|
</section>
|
||||||
|
{/if}
|
@ -41,6 +41,7 @@
|
|||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
import AlbumListItemDetails from './album-list-item-details.svelte';
|
import AlbumListItemDetails from './album-list-item-details.svelte';
|
||||||
import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
|
import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
|
||||||
|
import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
@ -162,6 +163,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<DetailPanelDescription {asset} {isOwner} />
|
<DetailPanelDescription {asset} {isOwner} />
|
||||||
|
<DetailPanelRating {asset} {isOwner} />
|
||||||
|
|
||||||
{#if (!isSharedLink() && unassignedFaces.length > 0) || people.length > 0}
|
{#if (!isSharedLink() && unassignedFaces.length > 0) || people.length > 0}
|
||||||
<section class="px-4 py-4 text-sm">
|
<section class="px-4 py-4 text-sm">
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
export let ariaHidden: boolean | undefined = undefined;
|
export let ariaHidden: boolean | undefined = undefined;
|
||||||
export let ariaLabel: string | undefined = undefined;
|
export let ariaLabel: string | undefined = undefined;
|
||||||
export let ariaLabelledby: string | undefined = undefined;
|
export let ariaLabelledby: string | undefined = undefined;
|
||||||
|
export let strokeWidth: number = 0;
|
||||||
|
export let strokeColor: string = 'currentColor';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
@ -22,6 +24,8 @@
|
|||||||
{viewBox}
|
{viewBox}
|
||||||
class="{className} {flipped ? '-scale-x-100' : ''}"
|
class="{className} {flipped ? '-scale-x-100' : ''}"
|
||||||
{role}
|
{role}
|
||||||
|
stroke={strokeColor}
|
||||||
|
stroke-width={strokeWidth}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
aria-hidden={ariaHidden}
|
aria-hidden={ariaHidden}
|
||||||
aria-labelledby={ariaLabelledby}
|
aria-labelledby={ariaLabelledby}
|
||||||
|
50
web/src/lib/components/shared-components/star-rating.svelte
Normal file
50
web/src/lib/components/shared-components/star-rating.svelte
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
|
||||||
|
export let count = 5;
|
||||||
|
export let rating: number;
|
||||||
|
export let readOnly = false;
|
||||||
|
export let onRating: (rating: number) => void | undefined;
|
||||||
|
|
||||||
|
let hoverRating = 0;
|
||||||
|
|
||||||
|
const starIcon =
|
||||||
|
'M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z';
|
||||||
|
|
||||||
|
const handleSelect = (newRating: number) => {
|
||||||
|
if (readOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newRating === rating) {
|
||||||
|
newRating = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
rating = newRating;
|
||||||
|
|
||||||
|
onRating?.(rating);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div role="presentation" tabindex="-1" on:mouseout={() => (hoverRating = 0)} on:blur|preventDefault>
|
||||||
|
{#each { length: count } as _, index}
|
||||||
|
{@const value = index + 1}
|
||||||
|
{@const filled = hoverRating >= value || (hoverRating === 0 && rating >= value)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => handleSelect(value)}
|
||||||
|
on:mouseover={() => (hoverRating = value)}
|
||||||
|
on:focus|preventDefault={() => (hoverRating = value)}
|
||||||
|
class="shadow-0 outline-0 text-immich-primary dark:text-immich-dark-primary"
|
||||||
|
disabled={readOnly}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
path={starIcon}
|
||||||
|
size="1.5em"
|
||||||
|
strokeWidth={1}
|
||||||
|
color={filled ? 'currentcolor' : 'transparent'}
|
||||||
|
strokeColor={filled ? 'currentcolor' : '#c1cce8'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
@ -19,6 +19,13 @@
|
|||||||
import { locale as i18nLocale, t } from 'svelte-i18n';
|
import { locale as i18nLocale, t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { invalidateAll } from '$app/navigation';
|
import { invalidateAll } from '$app/navigation';
|
||||||
|
import { preferences } from '$lib/stores/user.store';
|
||||||
|
import { updateMyPreferences } from '@immich/sdk';
|
||||||
|
import { handleError } from '../../utils/handle-error';
|
||||||
|
import {
|
||||||
|
notificationController,
|
||||||
|
NotificationType,
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
|
||||||
let time = new Date();
|
let time = new Date();
|
||||||
|
|
||||||
@ -39,6 +46,7 @@
|
|||||||
label: findLocale(editedLocale).name || fallbackLocale.name,
|
label: findLocale(editedLocale).name || fallbackLocale.name,
|
||||||
};
|
};
|
||||||
$: closestLanguage = getClosestAvailableLocale([$lang], langCodes);
|
$: closestLanguage = getClosestAvailableLocale([$lang], langCodes);
|
||||||
|
$: ratingEnabled = $preferences?.rating?.enabled;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
@ -90,6 +98,17 @@
|
|||||||
$locale = newLocale;
|
$locale = newLocale;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRatingChange = async (enabled: boolean) => {
|
||||||
|
try {
|
||||||
|
const data = await updateMyPreferences({ userPreferencesUpdateDto: { rating: { enabled } } });
|
||||||
|
$preferences.rating.enabled = data.rating.enabled;
|
||||||
|
|
||||||
|
notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info });
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_update_settings'));
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="my-4">
|
<section class="my-4">
|
||||||
@ -185,6 +204,14 @@
|
|||||||
bind:checked={$sidebarSettings.sharing}
|
bind:checked={$sidebarSettings.sharing}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<SettingSwitch
|
||||||
|
title={$t('rating')}
|
||||||
|
subtitle={$t('rating_description')}
|
||||||
|
bind:checked={ratingEnabled}
|
||||||
|
on:toggle={({ detail: enabled }) => handleRatingChange(enabled)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -1021,6 +1021,8 @@
|
|||||||
"purchase_server_title": "Server",
|
"purchase_server_title": "Server",
|
||||||
"purchase_settings_server_activated": "Der Server Produktschlüssel wird durch den Administrator verwaltet",
|
"purchase_settings_server_activated": "Der Server Produktschlüssel wird durch den Administrator verwaltet",
|
||||||
"range": "Reichweite",
|
"range": "Reichweite",
|
||||||
|
"rating": "Bewertung",
|
||||||
|
"rating_description": "Stellt die Exif-Bewertung im Informationsbereich dar",
|
||||||
"raw": "RAW",
|
"raw": "RAW",
|
||||||
"reaction_options": "Reaktionsmöglichkeiten",
|
"reaction_options": "Reaktionsmöglichkeiten",
|
||||||
"read_changelog": "Changelog lesen",
|
"read_changelog": "Changelog lesen",
|
||||||
|
@ -957,6 +957,8 @@
|
|||||||
"purchase_server_description_2": "Supporter status",
|
"purchase_server_description_2": "Supporter status",
|
||||||
"purchase_server_title": "Server",
|
"purchase_server_title": "Server",
|
||||||
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
||||||
|
"rating": "Star rating",
|
||||||
|
"rating_description": "Display the exif rating in the info panel",
|
||||||
"reaction_options": "Reaction options",
|
"reaction_options": "Reaction options",
|
||||||
"read_changelog": "Read Changelog",
|
"read_changelog": "Read Changelog",
|
||||||
"reassign": "Reassign",
|
"reassign": "Reassign",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user