From ae49951605ff1affd4efffdc53542227ca387065 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 9 Aug 2024 11:29:43 -0400 Subject: [PATCH] chore: clean up --- mobile/openapi/README.md | 2 + mobile/openapi/lib/api.dart | 2 + mobile/openapi/lib/api_client.dart | 4 + .../lib/model/asset_bulk_update_dto.dart | 21 +++- .../openapi/lib/model/exif_response_dto.dart | 15 ++- mobile/openapi/lib/model/rating_response.dart | 98 ++++++++++++++++ mobile/openapi/lib/model/rating_update.dart | 107 ++++++++++++++++++ .../openapi/lib/model/update_asset_dto.dart | 25 +++- .../model/user_preferences_response_dto.dart | 14 ++- .../model/user_preferences_update_dto.dart | 23 +++- open-api/immich-openapi-specs.json | 42 ++++++- open-api/typescript-sdk/src/fetch-client.ts | 11 ++ server/src/dtos/user-preferences.dto.ts | 20 ++-- server/src/entities/user-metadata.entity.ts | 8 +- server/src/queries/asset.repository.sql | 7 ++ server/src/queries/person.repository.sql | 1 + server/src/queries/search.repository.sql | 1 + server/src/queries/shared.link.repository.sql | 2 + .../detail-panel-star-rating.svelte | 25 ++-- .../asset-viewer/detail-panel.svelte | 5 +- web/src/lib/components/elements/icon.svelte | 4 + .../shared-components/star-rating.svelte | 85 +++++++------- .../user-settings-page/app-settings.svelte | 13 +-- 23 files changed, 432 insertions(+), 103 deletions(-) create mode 100644 mobile/openapi/lib/model/rating_response.dart create mode 100644 mobile/openapi/lib/model/rating_update.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 52e2e3cb40624..d2944426bf80c 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -371,6 +371,8 @@ Class | Method | HTTP request | Description - [PurchaseResponse](doc//PurchaseResponse.md) - [PurchaseUpdate](doc//PurchaseUpdate.md) - [QueueStatusDto](doc//QueueStatusDto.md) + - [RatingResponse](doc//RatingResponse.md) + - [RatingUpdate](doc//RatingUpdate.md) - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index e7aaf38de70ca..19ff7fc6d56e4 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -183,6 +183,8 @@ part 'model/places_response_dto.dart'; part 'model/purchase_response.dart'; part 'model/purchase_update.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_type.dart'; part 'model/reverse_geocoding_state_response_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 4fe810b886e47..346eee3f5043d 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -424,6 +424,10 @@ class ApiClient { return PurchaseUpdate.fromJson(value); case 'QueueStatusDto': return QueueStatusDto.fromJson(value); + case 'RatingResponse': + return RatingResponse.fromJson(value); + case 'RatingUpdate': + return RatingUpdate.fromJson(value); case 'ReactionLevel': return ReactionLevelTypeTransformer().decode(value); case 'ReactionType': diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index dcab64e1f380f..452dd2f9a51f1 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -20,6 +20,7 @@ class AssetBulkUpdateDto { this.isFavorite, this.latitude, this.longitude, + this.rating, this.removeParent, this.stackParentId, }); @@ -68,6 +69,16 @@ class AssetBulkUpdateDto { /// 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 /// does not include a default value (using the "default:" property), however, the generated @@ -93,6 +104,7 @@ class AssetBulkUpdateDto { other.isFavorite == isFavorite && other.latitude == latitude && other.longitude == longitude && + other.rating == rating && other.removeParent == removeParent && other.stackParentId == stackParentId; @@ -106,11 +118,12 @@ class AssetBulkUpdateDto { (isFavorite == null ? 0 : isFavorite!.hashCode) + (latitude == null ? 0 : latitude!.hashCode) + (longitude == null ? 0 : longitude!.hashCode) + + (rating == null ? 0 : rating!.hashCode) + (removeParent == null ? 0 : removeParent!.hashCode) + (stackParentId == null ? 0 : stackParentId!.hashCode); @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 toJson() { final json = {}; @@ -145,6 +158,11 @@ class AssetBulkUpdateDto { } else { // json[r'longitude'] = null; } + if (this.rating != null) { + json[r'rating'] = this.rating; + } else { + // json[r'rating'] = null; + } if (this.removeParent != null) { json[r'removeParent'] = this.removeParent; } else { @@ -175,6 +193,7 @@ class AssetBulkUpdateDto { isFavorite: mapValueOfType(json, r'isFavorite'), latitude: num.parse('${json[r'latitude']}'), longitude: num.parse('${json[r'longitude']}'), + rating: num.parse('${json[r'rating']}'), removeParent: mapValueOfType(json, r'removeParent'), stackParentId: mapValueOfType(json, r'stackParentId'), ); diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart index d29d485a057f5..0185f300fac5b 100644 --- a/mobile/openapi/lib/model/exif_response_dto.dart +++ b/mobile/openapi/lib/model/exif_response_dto.dart @@ -32,6 +32,7 @@ class ExifResponseDto { this.modifyDate, this.orientation, this.projectionType, + this.rating, this.state, this.timeZone, }); @@ -74,6 +75,8 @@ class ExifResponseDto { String? projectionType; + num? rating; + String? state; String? timeZone; @@ -99,6 +102,7 @@ class ExifResponseDto { other.modifyDate == modifyDate && other.orientation == orientation && other.projectionType == projectionType && + other.rating == rating && other.state == state && other.timeZone == timeZone; @@ -124,11 +128,12 @@ class ExifResponseDto { (modifyDate == null ? 0 : modifyDate!.hashCode) + (orientation == null ? 0 : orientation!.hashCode) + (projectionType == null ? 0 : projectionType!.hashCode) + + (rating == null ? 0 : rating!.hashCode) + (state == null ? 0 : state!.hashCode) + (timeZone == null ? 0 : timeZone!.hashCode); @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 toJson() { final json = {}; @@ -227,6 +232,11 @@ class ExifResponseDto { } else { // json[r'projectionType'] = null; } + if (this.rating != null) { + json[r'rating'] = this.rating; + } else { + // json[r'rating'] = null; + } if (this.state != null) { json[r'state'] = this.state; } else { @@ -281,6 +291,9 @@ class ExifResponseDto { modifyDate: mapDateTime(json, r'modifyDate', r''), orientation: mapValueOfType(json, r'orientation'), projectionType: mapValueOfType(json, r'projectionType'), + rating: json[r'rating'] == null + ? null + : num.parse('${json[r'rating']}'), state: mapValueOfType(json, r'state'), timeZone: mapValueOfType(json, r'timeZone'), ); diff --git a/mobile/openapi/lib/model/rating_response.dart b/mobile/openapi/lib/model/rating_response.dart new file mode 100644 index 0000000000000..80ef5980fb2e2 --- /dev/null +++ b/mobile/openapi/lib/model/rating_response.dart @@ -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 toJson() { + final json = {}; + 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(); + + return RatingResponse( + enabled: mapValueOfType(json, r'enabled')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + 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 mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // 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> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + 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 = { + 'enabled', + }; +} + diff --git a/mobile/openapi/lib/model/rating_update.dart b/mobile/openapi/lib/model/rating_update.dart new file mode 100644 index 0000000000000..bb8f7eadc2f55 --- /dev/null +++ b/mobile/openapi/lib/model/rating_update.dart @@ -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 toJson() { + final json = {}; + 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(); + + return RatingUpdate( + enabled: mapValueOfType(json, r'enabled'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + 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 mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // 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> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + 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 = { + }; +} + diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index e9a4d8d6b8cd1..391836c444bb3 100644 --- a/mobile/openapi/lib/model/update_asset_dto.dart +++ b/mobile/openapi/lib/model/update_asset_dto.dart @@ -19,6 +19,7 @@ class UpdateAssetDto { this.isFavorite, this.latitude, this.longitude, + this.rating, }); /// @@ -69,6 +70,16 @@ class UpdateAssetDto { /// 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 bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto && other.dateTimeOriginal == dateTimeOriginal && @@ -76,7 +87,8 @@ class UpdateAssetDto { other.isArchived == isArchived && other.isFavorite == isFavorite && other.latitude == latitude && - other.longitude == longitude; + other.longitude == longitude && + other.rating == rating; @override int get hashCode => @@ -86,10 +98,11 @@ class UpdateAssetDto { (isArchived == null ? 0 : isArchived!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (latitude == null ? 0 : latitude!.hashCode) + - (longitude == null ? 0 : longitude!.hashCode); + (longitude == null ? 0 : longitude!.hashCode) + + (rating == null ? 0 : rating!.hashCode); @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 toJson() { final json = {}; @@ -123,6 +136,11 @@ class UpdateAssetDto { } else { // json[r'longitude'] = null; } + if (this.rating != null) { + json[r'rating'] = this.rating; + } else { + // json[r'rating'] = null; + } return json; } @@ -140,6 +158,7 @@ class UpdateAssetDto { isFavorite: mapValueOfType(json, r'isFavorite'), latitude: num.parse('${json[r'latitude']}'), longitude: num.parse('${json[r'longitude']}'), + rating: num.parse('${json[r'rating']}'), ); } return null; diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index 21b96bb557eec..6401a36f9fda2 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -18,6 +18,7 @@ class UserPreferencesResponseDto { required this.emailNotifications, required this.memories, required this.purchase, + required this.rating, }); AvatarResponse avatar; @@ -30,13 +31,16 @@ class UserPreferencesResponseDto { PurchaseResponse purchase; + RatingResponse rating; + @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && other.avatar == avatar && other.download == download && other.emailNotifications == emailNotifications && other.memories == memories && - other.purchase == purchase; + other.purchase == purchase && + other.rating == rating; @override int get hashCode => @@ -45,10 +49,11 @@ class UserPreferencesResponseDto { (download.hashCode) + (emailNotifications.hashCode) + (memories.hashCode) + - (purchase.hashCode); + (purchase.hashCode) + + (rating.hashCode); @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 toJson() { final json = {}; @@ -57,6 +62,7 @@ class UserPreferencesResponseDto { json[r'emailNotifications'] = this.emailNotifications; json[r'memories'] = this.memories; json[r'purchase'] = this.purchase; + json[r'rating'] = this.rating; return json; } @@ -73,6 +79,7 @@ class UserPreferencesResponseDto { emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, memories: MemoryResponse.fromJson(json[r'memories'])!, purchase: PurchaseResponse.fromJson(json[r'purchase'])!, + rating: RatingResponse.fromJson(json[r'rating'])!, ); } return null; @@ -125,6 +132,7 @@ class UserPreferencesResponseDto { 'emailNotifications', 'memories', 'purchase', + 'rating', }; } diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index 616883a60a264..cf55aebf97df7 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -18,6 +18,7 @@ class UserPreferencesUpdateDto { this.emailNotifications, this.memories, this.purchase, + this.rating, }); /// @@ -60,13 +61,22 @@ class UserPreferencesUpdateDto { /// 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 bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto && other.avatar == avatar && other.download == download && other.emailNotifications == emailNotifications && other.memories == memories && - other.purchase == purchase; + other.purchase == purchase && + other.rating == rating; @override int get hashCode => @@ -75,10 +85,11 @@ class UserPreferencesUpdateDto { (download == null ? 0 : download!.hashCode) + (emailNotifications == null ? 0 : emailNotifications!.hashCode) + (memories == null ? 0 : memories!.hashCode) + - (purchase == null ? 0 : purchase!.hashCode); + (purchase == null ? 0 : purchase!.hashCode) + + (rating == null ? 0 : rating!.hashCode); @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 toJson() { final json = {}; @@ -107,6 +118,11 @@ class UserPreferencesUpdateDto { } else { // json[r'purchase'] = null; } + if (this.rating != null) { + json[r'rating'] = this.rating; + } else { + // json[r'rating'] = null; + } return json; } @@ -123,6 +139,7 @@ class UserPreferencesUpdateDto { emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']), memories: MemoryUpdate.fromJson(json[r'memories']), purchase: PurchaseUpdate.fromJson(json[r'purchase']), + rating: RatingUpdate.fromJson(json[r'rating']), ); } return null; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index cd43186ff9b89..8857ce97d3dc5 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7545,6 +7545,8 @@ "type": "number" }, "rating": { + "maximum": 5, + "minimum": 0, "type": "number" }, "removeParent": { @@ -8673,11 +8675,6 @@ "nullable": true, "type": "number" }, - "rating": { - "default": null, - "nullable": true, - "type": "number" - }, "make": { "default": null, "nullable": true, @@ -8704,6 +8701,11 @@ "nullable": true, "type": "string" }, + "rating": { + "default": null, + "nullable": true, + "type": "number" + }, "state": { "default": null, "nullable": true, @@ -9907,6 +9909,25 @@ ], "type": "object" }, + "RatingResponse": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "RatingUpdate": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, "ReactionLevel": { "enum": [ "album", @@ -11569,6 +11590,8 @@ "type": "number" }, "rating": { + "maximum": 5, + "minimum": 0, "type": "number" } }, @@ -11870,6 +11893,9 @@ }, "purchase": { "$ref": "#/components/schemas/PurchaseResponse" + }, + "rating": { + "$ref": "#/components/schemas/RatingResponse" } }, "required": [ @@ -11877,7 +11903,8 @@ "download", "emailNotifications", "memories", - "purchase" + "purchase", + "rating" ], "type": "object" }, @@ -11897,6 +11924,9 @@ }, "purchase": { "$ref": "#/components/schemas/PurchaseUpdate" + }, + "rating": { + "$ref": "#/components/schemas/RatingUpdate" } }, "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index ec2a230f776bc..23a4ab5b59b13 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -99,12 +99,16 @@ export type PurchaseResponse = { hideBuyButtonUntil: string; showSupportBadge: boolean; }; +export type RatingResponse = { + enabled: boolean; +}; export type UserPreferencesResponseDto = { avatar: AvatarResponse; download: DownloadResponse; emailNotifications: EmailNotificationsResponse; memories: MemoryResponse; purchase: PurchaseResponse; + rating: RatingResponse; }; export type AvatarUpdate = { color?: UserAvatarColor; @@ -124,12 +128,16 @@ export type PurchaseUpdate = { hideBuyButtonUntil?: string; showSupportBadge?: boolean; }; +export type RatingUpdate = { + enabled?: boolean; +}; export type UserPreferencesUpdateDto = { avatar?: AvatarUpdate; download?: DownloadUpdate; emailNotifications?: EmailNotificationsUpdate; memories?: MemoryUpdate; purchase?: PurchaseUpdate; + rating?: RatingUpdate; }; export type AlbumUserResponseDto = { role: AlbumUserRole; @@ -155,6 +163,7 @@ export type ExifResponseDto = { modifyDate?: string | null; orientation?: string | null; projectionType?: string | null; + rating?: number | null; state?: string | null; timeZone?: string | null; }; @@ -330,6 +339,7 @@ export type AssetBulkUpdateDto = { isFavorite?: boolean; latitude?: number; longitude?: number; + rating?: number; removeParent?: boolean; stackParentId?: string; }; @@ -381,6 +391,7 @@ export type UpdateAssetDto = { isFavorite?: boolean; latitude?: number; longitude?: number; + rating?: number; }; export type AssetMediaReplaceDto = { assetData: Blob; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index ea576ed8ec00f..8c50d0058180a 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -4,11 +4,6 @@ import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-v import { UserAvatarColor, UserPreferences } from 'src/entities/user-metadata.entity'; import { Optional, ValidateBoolean } from 'src/validation'; -class AppSettingsUpdate { - @ValidateBoolean({ optional: true }) - rating?: boolean; -} - class AvatarUpdate { @Optional() @IsEnum(UserAvatarColor) @@ -21,6 +16,11 @@ class MemoryUpdate { enabled?: boolean; } +class RatingUpdate { + @ValidateBoolean({ optional: true }) + enabled?: boolean; +} + class EmailNotificationsUpdate { @ValidateBoolean({ optional: true }) enabled?: boolean; @@ -52,8 +52,8 @@ class PurchaseUpdate { export class UserPreferencesUpdateDto { @Optional() @ValidateNested() - @Type(() => AppSettingsUpdate) - app_settings?: AppSettingsUpdate; + @Type(() => RatingUpdate) + rating?: RatingUpdate; @Optional() @ValidateNested() @@ -86,8 +86,8 @@ class AvatarResponse { color!: UserAvatarColor; } -class AppSettingsResponse { - rating!: boolean; +class RatingResponse { + enabled!: boolean; } class MemoryResponse { @@ -111,7 +111,7 @@ class PurchaseResponse { } export class UserPreferencesResponseDto implements UserPreferences { - app_settings!: AppSettingsResponse; + rating!: RatingResponse; memories!: MemoryResponse; avatar!: AvatarResponse; emailNotifications!: EmailNotificationsResponse; diff --git a/server/src/entities/user-metadata.entity.ts b/server/src/entities/user-metadata.entity.ts index 97013c63fc83e..fe56193b48450 100644 --- a/server/src/entities/user-metadata.entity.ts +++ b/server/src/entities/user-metadata.entity.ts @@ -31,8 +31,8 @@ export enum UserAvatarColor { } export interface UserPreferences { - app_settings: { - rating: boolean; + rating: { + enabled: boolean; }; memories: { enabled: boolean; @@ -61,8 +61,8 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences ); return { - app_settings: { - rating: true, + rating: { + enabled: true, }, memories: { enabled: true, diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index ba0707cfe709d..98fb1d6999d8f 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -58,6 +58,7 @@ SELECT "exifInfo"."profileDescription" AS "exifInfo_profileDescription", "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", "exifInfo"."fps" AS "exifInfo_fps" FROM "assets" "entity" @@ -177,6 +178,7 @@ SELECT "AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription", "AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace", "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_smartInfo"."assetId" AS "AssetEntity__AssetEntity_smartInfo_assetId", "AssetEntity__AssetEntity_smartInfo"."tags" AS "AssetEntity__AssetEntity_smartInfo_tags", @@ -628,6 +630,7 @@ SELECT "exifInfo"."profileDescription" AS "exifInfo_profileDescription", "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", "exifInfo"."fps" AS "exifInfo_fps", "stack"."id" AS "stack_id", "stack"."ownerId" AS "stack_ownerId", @@ -769,6 +772,7 @@ SELECT "exifInfo"."profileDescription" AS "exifInfo_profileDescription", "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", "exifInfo"."fps" AS "exifInfo_fps", "stack"."id" AS "stack_id", "stack"."ownerId" AS "stack_ownerId", @@ -886,6 +890,7 @@ SELECT "exifInfo"."profileDescription" AS "exifInfo_profileDescription", "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", "exifInfo"."fps" AS "exifInfo_fps", "stack"."id" AS "stack_id", "stack"."ownerId" AS "stack_ownerId", @@ -1053,6 +1058,7 @@ SELECT "exifInfo"."profileDescription" AS "exifInfo_profileDescription", "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", "exifInfo"."fps" AS "exifInfo_fps", "stack"."id" AS "stack_id", "stack"."ownerId" AS "stack_ownerId", @@ -1129,6 +1135,7 @@ SELECT "exifInfo"."profileDescription" AS "exifInfo_profileDescription", "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", "exifInfo"."fps" AS "exifInfo_fps", "stack"."id" AS "stack_id", "stack"."ownerId" AS "stack_ownerId", diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 4e4d36da8bf44..9b20b964d8eb3 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -322,6 +322,7 @@ FROM "AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription", "AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace", "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" FROM "assets" "AssetEntity" diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index 58a288a0cd875..390aedaf35017 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -402,6 +402,7 @@ SELECT "exif"."profileDescription" AS "exif_profileDescription", "exif"."colorspace" AS "exif_colorspace", "exif"."bitsPerSample" AS "exif_bitsPerSample", + "exif"."rating" AS "exif_rating", "exif"."fps" AS "exif_fps" FROM "assets" "asset" diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql index 09f0cf7cb5e3f..2880e6896f506 100644 --- a/server/src/queries/shared.link.repository.sql +++ b/server/src/queries/shared.link.repository.sql @@ -77,6 +77,7 @@ FROM "9b1d35b344d838023994a3233afd6ffe098be6d8"."profileDescription" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_profileDescription", "9b1d35b344d838023994a3233afd6ffe098be6d8"."colorspace" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_colorspace", "9b1d35b344d838023994a3233afd6ffe098be6d8"."bitsPerSample" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_bitsPerSample", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."rating" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_rating", "9b1d35b344d838023994a3233afd6ffe098be6d8"."fps" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_fps", "SharedLinkEntity__SharedLinkEntity_album"."id" AS "SharedLinkEntity__SharedLinkEntity_album_id", "SharedLinkEntity__SharedLinkEntity_album"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_album_ownerId", @@ -144,6 +145,7 @@ FROM "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."profileDescription" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_profileDescription", "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."colorspace" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_colorspace", "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."bitsPerSample" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_bitsPerSample", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."rating" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_rating", "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."fps" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_fps", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name", diff --git a/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte b/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte index 8828f24b70aa6..131d2ca43675f 100644 --- a/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte @@ -3,32 +3,25 @@ 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; - const countStars = 5; - $: rating = asset.exifInfo?.rating || 0; - let currentRating = rating; - const handleChangeRating = async (clickedId: CustomEvent) => { - currentRating = clickedId.detail; + const handleChangeRating = async (rating: number) => { try { - await updateAsset({ - id: asset.id, - updateAssetDto: { rating: currentRating === asset.exifInfo?.rating ? 0 : currentRating }, - }); + await updateAsset({ id: asset.id, updateAssetDto: { rating } }); } catch (error) { handleError(error, $t('errors.cant_apply_changes')); } }; -
- {#each { length: countStars } as _, id} - - - - {/each} -
+{#if !isSharedLink() && $preferences?.rating?.enabled} +
+ handlePromiseError(handleChangeRating(rating))} /> +
+{/if} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 157ecf1c97dc0..35dd6d0536ecb 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -7,7 +7,6 @@ import { locale } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { user } from '$lib/stores/user.store'; - import { preferences } from '$lib/stores/user.store'; import { websocketEvents } from '$lib/stores/websocket'; import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError, isSharedLink } from '$lib/utils'; import { delay, isFlipped } from '$lib/utils/asset-utils'; @@ -163,9 +162,7 @@ {/if} - {#if !isSharedLink() && $preferences?.app_settings?.rating} - - {/if} + {#if (!isSharedLink() && unassignedFaces.length > 0) || people.length > 0}
diff --git a/web/src/lib/components/elements/icon.svelte b/web/src/lib/components/elements/icon.svelte index bb8377e653761..bb22276286f11 100644 --- a/web/src/lib/components/elements/icon.svelte +++ b/web/src/lib/components/elements/icon.svelte @@ -14,6 +14,8 @@ export let ariaHidden: boolean | undefined = undefined; export let ariaLabel: string | undefined = undefined; export let ariaLabelledby: string | undefined = undefined; + export let strokeWidth: number = 0; + export let strokeColor: string = 'currentColor'; - import { createEventDispatcher } from 'svelte'; + import Icon from '$lib/components/elements/icon.svelte'; + export let count = 5; export let rating: number; - export let id: number; - export let readOnly: boolean = false; + export let readOnly = false; + export let onRating: (rating: number) => void | undefined; - let strokeWidth = 1; + let hoverRating = 0; - const dispatch = createEventDispatcher<{ click: number }>(); - const onClick = () => { - dispatch('click', id + 1); - }; + 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; + } - function handleMouseOver() { - strokeWidth = 2; - } + if (newRating === rating) { + newRating = 0; + } - function handleMouseOut() { - strokeWidth = 1; - } + rating = newRating; + + onRating?.(rating); + }; - +
(hoverRating = 0)} on:blur|preventDefault> + {#each { length: count } as _, index} + {@const value = index + 1} + {@const filled = hoverRating >= value || (hoverRating === 0 && rating >= value)} + + {/each} +
diff --git a/web/src/lib/components/user-settings-page/app-settings.svelte b/web/src/lib/components/user-settings-page/app-settings.svelte index 74d8963c0506e..cd1177d279464 100644 --- a/web/src/lib/components/user-settings-page/app-settings.svelte +++ b/web/src/lib/components/user-settings-page/app-settings.svelte @@ -46,6 +46,7 @@ label: findLocale(editedLocale).name || fallbackLocale.name, }; $: closestLanguage = getClosestAvailableLocale([$lang], langCodes); + $: ratingEnabled = $preferences?.rating?.enabled; onMount(() => { const interval = setInterval(() => { @@ -98,14 +99,10 @@ } }; - let ratingEnabled = $preferences?.app_settings?.rating; - - const handleRatingChange = async (newRating: CustomEvent) => { + const handleRatingChange = async (enabled: boolean) => { try { - const data = await updateMyPreferences({ - userPreferencesUpdateDto: { app_settings: { rating: newRating.detail } }, - }); - $preferences.app_settings.rating = data.app_settings.rating; + const data = await updateMyPreferences({ userPreferencesUpdateDto: { rating: { enabled } } }); + $preferences.rating.enabled = data.rating.enabled; notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info }); } catch (error) { @@ -212,7 +209,7 @@ title={$t('rating')} subtitle={$t('rating_description')} bind:checked={ratingEnabled} - on:toggle={handleRatingChange} + on:toggle={({ detail: enabled }) => handleRatingChange(enabled)} />