mirror of
https://github.com/immich-app/immich.git
synced 2026-03-22 11:29:38 +03:00
refactor: star rating (#26357)
* refactor: star rating * transform rating 0 to null in controller dto * migrate rating 0 to null * deprecate rating -1 * rating type annotation * update Rating type
This commit is contained in:
@@ -1810,9 +1810,8 @@
|
|||||||
"rate_asset": "Rate Asset",
|
"rate_asset": "Rate Asset",
|
||||||
"rating": "Star rating",
|
"rating": "Star rating",
|
||||||
"rating_clear": "Clear rating",
|
"rating_clear": "Clear rating",
|
||||||
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
"rating_count": "{count, plural, =0 {Unrated} one {# star} other {# stars}}",
|
||||||
"rating_description": "Display the EXIF rating in the info panel",
|
"rating_description": "Display the EXIF rating in the info panel",
|
||||||
"rating_set": "Rating set to {rating, plural, one {# star} other {# stars}}",
|
|
||||||
"reaction_options": "Reaction options",
|
"reaction_options": "Reaction options",
|
||||||
"read_changelog": "Read Changelog",
|
"read_changelog": "Read Changelog",
|
||||||
"readonly_mode_disabled": "Read-only mode disabled",
|
"readonly_mode_disabled": "Read-only mode disabled",
|
||||||
|
|||||||
4
mobile/openapi/lib/api/search_api.dart
generated
4
mobile/openapi/lib/api/search_api.dart
generated
@@ -410,7 +410,7 @@ class SearchApi {
|
|||||||
/// Filter by person IDs
|
/// Filter by person IDs
|
||||||
///
|
///
|
||||||
/// * [num] rating:
|
/// * [num] rating:
|
||||||
/// Filter by rating
|
/// Filter by rating [1-5], or null for unrated
|
||||||
///
|
///
|
||||||
/// * [num] size:
|
/// * [num] size:
|
||||||
/// Number of results to return
|
/// Number of results to return
|
||||||
@@ -633,7 +633,7 @@ class SearchApi {
|
|||||||
/// Filter by person IDs
|
/// Filter by person IDs
|
||||||
///
|
///
|
||||||
/// * [num] rating:
|
/// * [num] rating:
|
||||||
/// Filter by rating
|
/// Filter by rating [1-5], or null for unrated
|
||||||
///
|
///
|
||||||
/// * [num] size:
|
/// * [num] size:
|
||||||
/// Number of results to return
|
/// Number of results to return
|
||||||
|
|||||||
12
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
12
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
@@ -86,16 +86,10 @@ class AssetBulkUpdateDto {
|
|||||||
///
|
///
|
||||||
num? longitude;
|
num? longitude;
|
||||||
|
|
||||||
/// Rating
|
/// Rating in range [1-5], or null for unrated
|
||||||
///
|
///
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// 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;
|
num? rating;
|
||||||
|
|
||||||
/// Time zone (IANA timezone)
|
/// Time zone (IANA timezone)
|
||||||
@@ -223,7 +217,9 @@ 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']}'),
|
rating: json[r'rating'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'rating']}'),
|
||||||
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
||||||
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
||||||
);
|
);
|
||||||
|
|||||||
12
mobile/openapi/lib/model/metadata_search_dto.dart
generated
12
mobile/openapi/lib/model/metadata_search_dto.dart
generated
@@ -256,16 +256,10 @@ class MetadataSearchDto {
|
|||||||
///
|
///
|
||||||
String? previewPath;
|
String? previewPath;
|
||||||
|
|
||||||
/// Filter by rating
|
/// Filter by rating [1-5], or null for unrated
|
||||||
///
|
///
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// 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;
|
num? rating;
|
||||||
|
|
||||||
/// Number of results to return
|
/// Number of results to return
|
||||||
@@ -754,7 +748,9 @@ class MetadataSearchDto {
|
|||||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
previewPath: mapValueOfType<String>(json, r'previewPath'),
|
previewPath: mapValueOfType<String>(json, r'previewPath'),
|
||||||
rating: num.parse('${json[r'rating']}'),
|
rating: json[r'rating'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'rating']}'),
|
||||||
size: num.parse('${json[r'size']}'),
|
size: num.parse('${json[r'size']}'),
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
tagIds: json[r'tagIds'] is Iterable
|
tagIds: json[r'tagIds'] is Iterable
|
||||||
|
|||||||
12
mobile/openapi/lib/model/random_search_dto.dart
generated
12
mobile/openapi/lib/model/random_search_dto.dart
generated
@@ -159,16 +159,10 @@ class RandomSearchDto {
|
|||||||
/// Filter by person IDs
|
/// Filter by person IDs
|
||||||
List<String> personIds;
|
List<String> personIds;
|
||||||
|
|
||||||
/// Filter by rating
|
/// Filter by rating [1-5], or null for unrated
|
||||||
///
|
///
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// 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;
|
num? rating;
|
||||||
|
|
||||||
/// Number of results to return
|
/// Number of results to return
|
||||||
@@ -565,7 +559,9 @@ class RandomSearchDto {
|
|||||||
personIds: json[r'personIds'] is Iterable
|
personIds: json[r'personIds'] is Iterable
|
||||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
rating: num.parse('${json[r'rating']}'),
|
rating: json[r'rating'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'rating']}'),
|
||||||
size: num.parse('${json[r'size']}'),
|
size: num.parse('${json[r'size']}'),
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
tagIds: json[r'tagIds'] is Iterable
|
tagIds: json[r'tagIds'] is Iterable
|
||||||
|
|||||||
12
mobile/openapi/lib/model/smart_search_dto.dart
generated
12
mobile/openapi/lib/model/smart_search_dto.dart
generated
@@ -199,16 +199,10 @@ class SmartSearchDto {
|
|||||||
///
|
///
|
||||||
String? queryAssetId;
|
String? queryAssetId;
|
||||||
|
|
||||||
/// Filter by rating
|
/// Filter by rating [1-5], or null for unrated
|
||||||
///
|
///
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// 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;
|
num? rating;
|
||||||
|
|
||||||
/// Number of results to return
|
/// Number of results to return
|
||||||
@@ -605,7 +599,9 @@ class SmartSearchDto {
|
|||||||
: const [],
|
: const [],
|
||||||
query: mapValueOfType<String>(json, r'query'),
|
query: mapValueOfType<String>(json, r'query'),
|
||||||
queryAssetId: mapValueOfType<String>(json, r'queryAssetId'),
|
queryAssetId: mapValueOfType<String>(json, r'queryAssetId'),
|
||||||
rating: num.parse('${json[r'rating']}'),
|
rating: json[r'rating'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'rating']}'),
|
||||||
size: num.parse('${json[r'size']}'),
|
size: num.parse('${json[r'size']}'),
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
tagIds: json[r'tagIds'] is Iterable
|
tagIds: json[r'tagIds'] is Iterable
|
||||||
|
|||||||
12
mobile/openapi/lib/model/statistics_search_dto.dart
generated
12
mobile/openapi/lib/model/statistics_search_dto.dart
generated
@@ -164,16 +164,10 @@ class StatisticsSearchDto {
|
|||||||
/// Filter by person IDs
|
/// Filter by person IDs
|
||||||
List<String> personIds;
|
List<String> personIds;
|
||||||
|
|
||||||
/// Filter by rating
|
/// Filter by rating [1-5], or null for unrated
|
||||||
///
|
///
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// 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;
|
num? rating;
|
||||||
|
|
||||||
/// Filter by state/province name
|
/// Filter by state/province name
|
||||||
@@ -495,7 +489,9 @@ class StatisticsSearchDto {
|
|||||||
personIds: json[r'personIds'] is Iterable
|
personIds: json[r'personIds'] is Iterable
|
||||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
rating: num.parse('${json[r'rating']}'),
|
rating: json[r'rating'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'rating']}'),
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
tagIds: json[r'tagIds'] is Iterable
|
tagIds: json[r'tagIds'] is Iterable
|
||||||
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
|||||||
12
mobile/openapi/lib/model/update_asset_dto.dart
generated
12
mobile/openapi/lib/model/update_asset_dto.dart
generated
@@ -71,16 +71,10 @@ class UpdateAssetDto {
|
|||||||
///
|
///
|
||||||
num? longitude;
|
num? longitude;
|
||||||
|
|
||||||
/// Rating
|
/// Rating in range [1-5], or null for unrated
|
||||||
///
|
///
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// 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;
|
num? rating;
|
||||||
|
|
||||||
/// Asset visibility
|
/// Asset visibility
|
||||||
@@ -178,7 +172,9 @@ class UpdateAssetDto {
|
|||||||
latitude: num.parse('${json[r'latitude']}'),
|
latitude: num.parse('${json[r'latitude']}'),
|
||||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||||
longitude: num.parse('${json[r'longitude']}'),
|
longitude: num.parse('${json[r'longitude']}'),
|
||||||
rating: num.parse('${json[r'rating']}'),
|
rating: json[r'rating'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'rating']}'),
|
||||||
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9407,10 +9407,27 @@
|
|||||||
"name": "rating",
|
"name": "rating",
|
||||||
"required": false,
|
"required": false,
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"description": "Filter by rating",
|
"description": "Filter by rating [1-5], or null for unrated",
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v1",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2",
|
||||||
|
"state": "Stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2.6.0",
|
||||||
|
"state": "Updated",
|
||||||
|
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-state": "Stable",
|
||||||
"schema": {
|
"schema": {
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
|
"nullable": true,
|
||||||
"type": "number"
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -15872,10 +15889,27 @@
|
|||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"rating": {
|
"rating": {
|
||||||
"description": "Rating",
|
"description": "Rating in range [1-5], or null for unrated",
|
||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
"type": "number"
|
"nullable": true,
|
||||||
|
"type": "number",
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v1",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2",
|
||||||
|
"state": "Stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2.6.0",
|
||||||
|
"state": "Updated",
|
||||||
|
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-state": "Stable"
|
||||||
},
|
},
|
||||||
"timeZone": {
|
"timeZone": {
|
||||||
"description": "Time zone (IANA timezone)",
|
"description": "Time zone (IANA timezone)",
|
||||||
@@ -18988,10 +19022,27 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"rating": {
|
"rating": {
|
||||||
"description": "Filter by rating",
|
"description": "Filter by rating [1-5], or null for unrated",
|
||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
"type": "number"
|
"nullable": true,
|
||||||
|
"type": "number",
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v1",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2",
|
||||||
|
"state": "Stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2.6.0",
|
||||||
|
"state": "Updated",
|
||||||
|
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-state": "Stable"
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"description": "Number of results to return",
|
"description": "Number of results to return",
|
||||||
@@ -20714,10 +20765,27 @@
|
|||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"rating": {
|
"rating": {
|
||||||
"description": "Filter by rating",
|
"description": "Filter by rating [1-5], or null for unrated",
|
||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
"type": "number"
|
"nullable": true,
|
||||||
|
"type": "number",
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v1",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2",
|
||||||
|
"state": "Stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2.6.0",
|
||||||
|
"state": "Updated",
|
||||||
|
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-state": "Stable"
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"description": "Number of results to return",
|
"description": "Number of results to return",
|
||||||
@@ -22088,10 +22156,27 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"rating": {
|
"rating": {
|
||||||
"description": "Filter by rating",
|
"description": "Filter by rating [1-5], or null for unrated",
|
||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
"type": "number"
|
"nullable": true,
|
||||||
|
"type": "number",
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v1",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2",
|
||||||
|
"state": "Stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2.6.0",
|
||||||
|
"state": "Updated",
|
||||||
|
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-state": "Stable"
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"description": "Number of results to return",
|
"description": "Number of results to return",
|
||||||
@@ -22322,10 +22407,27 @@
|
|||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"rating": {
|
"rating": {
|
||||||
"description": "Filter by rating",
|
"description": "Filter by rating [1-5], or null for unrated",
|
||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
"type": "number"
|
"nullable": true,
|
||||||
|
"type": "number",
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v1",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2",
|
||||||
|
"state": "Stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2.6.0",
|
||||||
|
"state": "Updated",
|
||||||
|
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-state": "Stable"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"description": "Filter by state/province name",
|
"description": "Filter by state/province name",
|
||||||
@@ -25209,10 +25311,27 @@
|
|||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"rating": {
|
"rating": {
|
||||||
"description": "Rating",
|
"description": "Rating in range [1-5], or null for unrated",
|
||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
"type": "number"
|
"nullable": true,
|
||||||
|
"type": "number",
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v1",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2",
|
||||||
|
"state": "Stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2.6.0",
|
||||||
|
"state": "Updated",
|
||||||
|
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-state": "Stable"
|
||||||
},
|
},
|
||||||
"visibility": {
|
"visibility": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
|
|||||||
@@ -834,8 +834,8 @@ export type AssetBulkUpdateDto = {
|
|||||||
latitude?: number;
|
latitude?: number;
|
||||||
/** Longitude coordinate */
|
/** Longitude coordinate */
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
/** Rating */
|
/** Rating in range [1-5], or null for unrated */
|
||||||
rating?: number;
|
rating?: number | null;
|
||||||
/** Time zone (IANA timezone) */
|
/** Time zone (IANA timezone) */
|
||||||
timeZone?: string;
|
timeZone?: string;
|
||||||
/** Asset visibility */
|
/** Asset visibility */
|
||||||
@@ -944,8 +944,8 @@ export type UpdateAssetDto = {
|
|||||||
livePhotoVideoId?: string | null;
|
livePhotoVideoId?: string | null;
|
||||||
/** Longitude coordinate */
|
/** Longitude coordinate */
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
/** Rating */
|
/** Rating in range [1-5], or null for unrated */
|
||||||
rating?: number;
|
rating?: number | null;
|
||||||
/** Asset visibility */
|
/** Asset visibility */
|
||||||
visibility?: AssetVisibility;
|
visibility?: AssetVisibility;
|
||||||
};
|
};
|
||||||
@@ -1711,8 +1711,8 @@ export type MetadataSearchDto = {
|
|||||||
personIds?: string[];
|
personIds?: string[];
|
||||||
/** Filter by preview file path */
|
/** Filter by preview file path */
|
||||||
previewPath?: string;
|
previewPath?: string;
|
||||||
/** Filter by rating */
|
/** Filter by rating [1-5], or null for unrated */
|
||||||
rating?: number;
|
rating?: number | null;
|
||||||
/** Number of results to return */
|
/** Number of results to return */
|
||||||
size?: number;
|
size?: number;
|
||||||
/** Filter by state/province name */
|
/** Filter by state/province name */
|
||||||
@@ -1827,8 +1827,8 @@ export type RandomSearchDto = {
|
|||||||
ocr?: string;
|
ocr?: string;
|
||||||
/** Filter by person IDs */
|
/** Filter by person IDs */
|
||||||
personIds?: string[];
|
personIds?: string[];
|
||||||
/** Filter by rating */
|
/** Filter by rating [1-5], or null for unrated */
|
||||||
rating?: number;
|
rating?: number | null;
|
||||||
/** Number of results to return */
|
/** Number of results to return */
|
||||||
size?: number;
|
size?: number;
|
||||||
/** Filter by state/province name */
|
/** Filter by state/province name */
|
||||||
@@ -1903,8 +1903,8 @@ export type SmartSearchDto = {
|
|||||||
query?: string;
|
query?: string;
|
||||||
/** Asset ID to use as search reference */
|
/** Asset ID to use as search reference */
|
||||||
queryAssetId?: string;
|
queryAssetId?: string;
|
||||||
/** Filter by rating */
|
/** Filter by rating [1-5], or null for unrated */
|
||||||
rating?: number;
|
rating?: number | null;
|
||||||
/** Number of results to return */
|
/** Number of results to return */
|
||||||
size?: number;
|
size?: number;
|
||||||
/** Filter by state/province name */
|
/** Filter by state/province name */
|
||||||
@@ -1969,8 +1969,8 @@ export type StatisticsSearchDto = {
|
|||||||
ocr?: string;
|
ocr?: string;
|
||||||
/** Filter by person IDs */
|
/** Filter by person IDs */
|
||||||
personIds?: string[];
|
personIds?: string[];
|
||||||
/** Filter by rating */
|
/** Filter by rating [1-5], or null for unrated */
|
||||||
rating?: number;
|
rating?: number | null;
|
||||||
/** Filter by state/province name */
|
/** Filter by state/province name */
|
||||||
state?: string | null;
|
state?: string | null;
|
||||||
/** Filter by tag IDs */
|
/** Filter by tag IDs */
|
||||||
@@ -5454,7 +5454,7 @@ export function searchLargeAssets({ albumIds, city, country, createdAfter, creat
|
|||||||
model?: string | null;
|
model?: string | null;
|
||||||
ocr?: string;
|
ocr?: string;
|
||||||
personIds?: string[];
|
personIds?: string[];
|
||||||
rating?: number;
|
rating?: number | null;
|
||||||
size?: number;
|
size?: number;
|
||||||
state?: string | null;
|
state?: string | null;
|
||||||
tagIds?: string[] | null;
|
tagIds?: string[] | null;
|
||||||
|
|||||||
@@ -207,12 +207,28 @@ describe(AssetController.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reject invalid rating', async () => {
|
it('should reject invalid rating', async () => {
|
||||||
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
|
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: -2 }]) {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
|
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(factory.responses.badRequest());
|
expect(body).toEqual(factory.responses.badRequest());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should convert rating 0 to null', async () => {
|
||||||
|
const assetId = factory.uuid();
|
||||||
|
const { status } = await request(ctx.getHttpServer()).put(`/assets/${assetId}`).send({ rating: 0 });
|
||||||
|
expect(service.update).toHaveBeenCalledWith(undefined, assetId, { rating: null });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should leave correct ratings as-is', async () => {
|
||||||
|
const assetId = factory.uuid();
|
||||||
|
for (const test of [{ rating: -1 }, { rating: 1 }, { rating: 5 }]) {
|
||||||
|
const { status } = await request(ctx.getHttpServer()).put(`/assets/${assetId}`).send(test);
|
||||||
|
expect(service.update).toHaveBeenCalledWith(undefined, assetId, test);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /assets/statistics', () => {
|
describe('GET /assets/statistics', () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Transform, Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsArray,
|
IsArray,
|
||||||
IsDateString,
|
IsDateString,
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
ValidateIf,
|
ValidateIf,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
import { HistoryBuilder, Property } from 'src/decorators';
|
||||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AssetType, AssetVisibility } from 'src/enum';
|
import { AssetType, AssetVisibility } from 'src/enum';
|
||||||
import { AssetStats } from 'src/repositories/asset.repository';
|
import { AssetStats } from 'src/repositories/asset.repository';
|
||||||
@@ -56,12 +57,19 @@ export class UpdateAssetBase {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Rating' })
|
@Property({
|
||||||
@Optional()
|
description: 'Rating in range [1-5], or null for unrated',
|
||||||
|
history: new HistoryBuilder()
|
||||||
|
.added('v1')
|
||||||
|
.stable('v2')
|
||||||
|
.updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.'),
|
||||||
|
})
|
||||||
|
@Optional({ nullable: true })
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Max(5)
|
@Max(5)
|
||||||
@Min(-1)
|
@Min(-1)
|
||||||
rating?: number;
|
@Transform(({ value }) => (value === 0 ? null : value))
|
||||||
|
rating?: number | null;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Asset description' })
|
@ApiProperty({ description: 'Asset description' })
|
||||||
@Optional()
|
@Optional()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
|
import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
|
||||||
import { Place } from 'src/database';
|
import { Place } from 'src/database';
|
||||||
import { HistoryBuilder } from 'src/decorators';
|
import { HistoryBuilder, Property } from 'src/decorators';
|
||||||
import { AlbumResponseDto } from 'src/dtos/album.dto';
|
import { AlbumResponseDto } from 'src/dtos/album.dto';
|
||||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { AssetOrder, AssetType, AssetVisibility } from 'src/enum';
|
import { AssetOrder, AssetType, AssetVisibility } from 'src/enum';
|
||||||
@@ -103,12 +103,21 @@ class BaseSearchDto {
|
|||||||
@ValidateUUID({ each: true, optional: true, description: 'Filter by album IDs' })
|
@ValidateUUID({ each: true, optional: true, description: 'Filter by album IDs' })
|
||||||
albumIds?: string[];
|
albumIds?: string[];
|
||||||
|
|
||||||
@ApiPropertyOptional({ type: 'number', description: 'Filter by rating', minimum: -1, maximum: 5 })
|
@Property({
|
||||||
@Optional()
|
type: 'number',
|
||||||
|
description: 'Filter by rating [1-5], or null for unrated',
|
||||||
|
minimum: -1,
|
||||||
|
maximum: 5,
|
||||||
|
history: new HistoryBuilder()
|
||||||
|
.added('v1')
|
||||||
|
.stable('v2')
|
||||||
|
.updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.'),
|
||||||
|
})
|
||||||
|
@Optional({ nullable: true })
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Max(5)
|
@Max(5)
|
||||||
@Min(-1)
|
@Min(-1)
|
||||||
rating?: number;
|
rating?: number | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Filter by OCR text content' })
|
@ApiPropertyOptional({ description: 'Filter by OCR text content' })
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export class MediaRepository {
|
|||||||
ExposureTime: tags.exposureTime,
|
ExposureTime: tags.exposureTime,
|
||||||
ProfileDescription: tags.profileDescription,
|
ProfileDescription: tags.profileDescription,
|
||||||
ColorSpace: tags.colorspace,
|
ColorSpace: tags.colorspace,
|
||||||
Rating: tags.rating,
|
Rating: tags.rating === null ? 0 : tags.rating,
|
||||||
// specially convert Orientation to numeric Orientation# for exiftool
|
// specially convert Orientation to numeric Orientation# for exiftool
|
||||||
'Orientation#': tags.orientation ? Number(tags.orientation) : undefined,
|
'Orientation#': tags.orientation ? Number(tags.orientation) : undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`UPDATE "asset_exif" SET "rating" = NULL WHERE "rating" = 0;`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(): Promise<void> {
|
||||||
|
// not supported
|
||||||
|
}
|
||||||
@@ -516,7 +516,7 @@ export class AssetService extends BaseService {
|
|||||||
dateTimeOriginal?: string;
|
dateTimeOriginal?: string;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
rating?: number;
|
rating?: number | null;
|
||||||
}) {
|
}) {
|
||||||
const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto;
|
const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto;
|
||||||
const writes = _.omitBy(
|
const writes = _.omitBy(
|
||||||
|
|||||||
@@ -1423,6 +1423,20 @@ describe(MetadataService.name, () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle 0 as unrated -> null', async () => {
|
||||||
|
const asset = AssetFactory.create();
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
|
mockReadTags({ Rating: 0 });
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
rating: null,
|
||||||
|
}),
|
||||||
|
{ lockedPropertiesBehavior: 'skip' },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle valid negative rating value', async () => {
|
it('should handle valid negative rating value', async () => {
|
||||||
const asset = AssetFactory.create();
|
const asset = AssetFactory.create();
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||||
@@ -1780,6 +1794,28 @@ describe(MetadataService.name, () => {
|
|||||||
'timeZone',
|
'timeZone',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should write rating', async () => {
|
||||||
|
const asset = factory.jobAssets.sidecarWrite();
|
||||||
|
asset.exifInfo.rating = 4;
|
||||||
|
|
||||||
|
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']);
|
||||||
|
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
|
||||||
|
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success);
|
||||||
|
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 4 });
|
||||||
|
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write null rating as 0', async () => {
|
||||||
|
const asset = factory.jobAssets.sidecarWrite();
|
||||||
|
asset.exifInfo.rating = null;
|
||||||
|
|
||||||
|
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']);
|
||||||
|
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
|
||||||
|
await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success);
|
||||||
|
expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 0 });
|
||||||
|
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('firstDateTime', () => {
|
describe('firstDateTime', () => {
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ export class MetadataService extends BaseService {
|
|||||||
// comments
|
// comments
|
||||||
description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
|
description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
|
||||||
profileDescription: exifTags.ProfileDescription || null,
|
profileDescription: exifTags.ProfileDescription || null,
|
||||||
rating: validateRange(exifTags.Rating, -1, 5),
|
rating: exifTags.Rating === 0 ? null : validateRange(exifTags.Rating, -1, 5),
|
||||||
|
|
||||||
// grouping
|
// grouping
|
||||||
livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null,
|
livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null,
|
||||||
@@ -451,7 +451,7 @@ export class MetadataService extends BaseService {
|
|||||||
dateTimeOriginal: asset.exifInfo.dateTimeOriginal as string | null,
|
dateTimeOriginal: asset.exifInfo.dateTimeOriginal as string | null,
|
||||||
latitude: asset.exifInfo.latitude,
|
latitude: asset.exifInfo.latitude,
|
||||||
longitude: asset.exifInfo.longitude,
|
longitude: asset.exifInfo.longitude,
|
||||||
rating: asset.exifInfo.rating,
|
rating: asset.exifInfo.rating ?? 0,
|
||||||
tags: asset.exifInfo.tags,
|
tags: asset.exifInfo.tags,
|
||||||
timeZone: asset.exifInfo.timeZone,
|
timeZone: asset.exifInfo.timeZone,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,10 +17,9 @@
|
|||||||
|
|
||||||
const rateAsset = async (rating: number | null) => {
|
const rateAsset = async (rating: number | null) => {
|
||||||
try {
|
try {
|
||||||
const updateAssetDto = rating === null ? {} : { rating };
|
|
||||||
await updateAsset({
|
await updateAsset({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
updateAssetDto,
|
updateAssetDto: { rating },
|
||||||
});
|
});
|
||||||
|
|
||||||
asset = {
|
asset = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import StarRating from '$lib/elements/StarRating.svelte';
|
import StarRating, { type Rating } from '$lib/elements/StarRating.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
|
|
||||||
let { asset, isOwner }: Props = $props();
|
let { asset, isOwner }: Props = $props();
|
||||||
|
|
||||||
let rating = $derived(asset.exifInfo?.rating || 0);
|
let rating = $derived(asset.exifInfo?.rating || null) as Rating;
|
||||||
|
|
||||||
const handleChangeRating = async (rating: number) => {
|
const handleChangeRating = async (rating: number | null) => {
|
||||||
try {
|
try {
|
||||||
await updateAsset({ id: asset.id, updateAssetDto: { rating } });
|
await updateAsset({ id: asset.id, updateAssetDto: { rating } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !authManager.isSharedLink && $preferences?.ratings.enabled}
|
{#if !authManager.isSharedLink && $preferences?.ratings.enabled}
|
||||||
<section class="px-4 pt-2">
|
<section class="px-4 pt-4">
|
||||||
<StarRating {rating} readOnly={!isOwner} onRating={(rating) => handlePromiseError(handleChangeRating(rating))} />
|
<StarRating {rating} readOnly={!isOwner} onRating={(rating) => handlePromiseError(handleChangeRating(rating))} />
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
import Combobox from '../combobox.svelte';
|
import Combobox from '../combobox.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
rating?: number;
|
rating?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { rating = $bindable() }: Props = $props();
|
let { rating = $bindable() }: Props = $props();
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{ value: '0', label: $t('rating_count', { values: { count: 0 } }) },
|
{ value: 'null', label: $t('rating_count', { values: { count: 0 } }) },
|
||||||
{ value: '1', label: $t('rating_count', { values: { count: 1 } }) },
|
{ value: '1', label: $t('rating_count', { values: { count: 1 } }) },
|
||||||
{ value: '2', label: $t('rating_count', { values: { count: 2 } }) },
|
{ value: '2', label: $t('rating_count', { values: { count: 2 } }) },
|
||||||
{ value: '3', label: $t('rating_count', { values: { count: 3 } }) },
|
{ value: '3', label: $t('rating_count', { values: { count: 3 } }) },
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
placeholder={$t('search_rating')}
|
placeholder={$t('search_rating')}
|
||||||
hideLabel
|
hideLabel
|
||||||
{options}
|
{options}
|
||||||
selectedOption={rating === undefined ? undefined : options[rating]}
|
selectedOption={rating === undefined ? undefined : options[rating === null ? 0 : rating]}
|
||||||
onSelect={(r) => (rating = r === undefined ? undefined : Number.parseInt(r.value))}
|
onSelect={(r) => (rating = r === undefined ? undefined : Number.parseInt(r.value))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,27 +3,28 @@
|
|||||||
import { shortcuts } from '$lib/actions/shortcut';
|
import { shortcuts } from '$lib/actions/shortcut';
|
||||||
import { generateId } from '$lib/utils/generate-id';
|
import { generateId } from '$lib/utils/generate-id';
|
||||||
import { Icon } from '@immich/ui';
|
import { Icon } from '@immich/ui';
|
||||||
|
import { mdiStar, mdiStarOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export type Rating = 1 | 2 | 3 | 4 | 5 | null;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
count?: number;
|
count?: number;
|
||||||
rating: number;
|
rating: Rating;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
onRating: (rating: number) => void | undefined;
|
onRating: (rating: Rating) => void | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { count = 5, rating, readOnly = false, onRating }: Props = $props();
|
let { count = 5, rating, readOnly = false, onRating }: Props = $props();
|
||||||
|
|
||||||
let ratingSelection = $derived(rating);
|
let ratingSelection = $derived(rating);
|
||||||
let hoverRating = $state(0);
|
let hoverRating: Rating = $state(null);
|
||||||
let focusRating = $state(0);
|
let focusRating: Rating = $state(null);
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
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 id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
const handleSelect = (newRating: number) => {
|
const handleSelect = (newRating: Rating) => {
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
onRating(newRating);
|
onRating(newRating);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setHoverRating = (value: number) => {
|
const setHoverRating = (value: Rating) => {
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -43,11 +44,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setHoverRating(0);
|
setHoverRating(null);
|
||||||
focusRating = 0;
|
focusRating = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectDebounced = (value: number) => {
|
const handleSelectDebounced = (value: Rating) => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
handleSelect(value);
|
handleSelect(value);
|
||||||
@@ -58,7 +59,7 @@
|
|||||||
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
||||||
<fieldset
|
<fieldset
|
||||||
class="text-primary w-fit cursor-default"
|
class="text-primary w-fit cursor-default"
|
||||||
onmouseleave={() => setHoverRating(0)}
|
onmouseleave={() => setHoverRating(null)}
|
||||||
use:focusOutside={{ onFocusOut: reset }}
|
use:focusOutside={{ onFocusOut: reset }}
|
||||||
use:shortcuts={[
|
use:shortcuts={[
|
||||||
{ shortcut: { key: 'ArrowLeft' }, preventDefault: false, onShortcut: (event) => event.stopPropagation() },
|
{ shortcut: { key: 'ArrowLeft' }, preventDefault: false, onShortcut: (event) => event.stopPropagation() },
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
<div class="flex flex-row" data-testid="star-container">
|
<div class="flex flex-row" data-testid="star-container">
|
||||||
{#each { length: count } as _, index (index)}
|
{#each { length: count } as _, index (index)}
|
||||||
{@const value = index + 1}
|
{@const value = index + 1}
|
||||||
{@const filled = hoverRating >= value || (hoverRating === 0 && ratingSelection >= value)}
|
{@const filled = hoverRating === null ? (ratingSelection || 0) >= value : hoverRating >= value}
|
||||||
{@const starId = `${id}-${value}`}
|
{@const starId = `${id}-${value}`}
|
||||||
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
@@ -77,19 +78,12 @@
|
|||||||
for={starId}
|
for={starId}
|
||||||
class:cursor-pointer={!readOnly}
|
class:cursor-pointer={!readOnly}
|
||||||
class:ring-2={focusRating === value}
|
class:ring-2={focusRating === value}
|
||||||
onmouseover={() => setHoverRating(value)}
|
onmouseover={() => setHoverRating(value as Rating)}
|
||||||
tabindex={-1}
|
tabindex={-1}
|
||||||
data-testid="star"
|
data-testid="star"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{$t('rating_count', { values: { count: value } })}</span>
|
<span class="sr-only">{$t('rating_count', { values: { count: value } })}</span>
|
||||||
<Icon
|
<Icon icon={filled ? mdiStar : mdiStarOutline} size="1.5em" aria-hidden />
|
||||||
icon={starIcon}
|
|
||||||
size="1.5em"
|
|
||||||
strokeWidth={1}
|
|
||||||
color={filled ? 'currentcolor' : 'transparent'}
|
|
||||||
strokeColor={filled ? 'currentcolor' : '#c1cce8'}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -99,19 +93,19 @@
|
|||||||
bind:group={ratingSelection}
|
bind:group={ratingSelection}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
onfocus={() => {
|
onfocus={() => {
|
||||||
focusRating = value;
|
focusRating = value as Rating;
|
||||||
}}
|
}}
|
||||||
onchange={() => handleSelectDebounced(value)}
|
onchange={() => handleSelectDebounced(value as Rating)}
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{#if ratingSelection > 0 && !readOnly}
|
{#if ratingSelection !== null && !readOnly}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
ratingSelection = 0;
|
ratingSelection = null;
|
||||||
handleSelect(ratingSelection);
|
handleSelect(ratingSelection);
|
||||||
}}
|
}}
|
||||||
class="cursor-pointer text-xs text-primary"
|
class="cursor-pointer text-xs text-primary"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
date: SearchDateFilter;
|
date: SearchDateFilter;
|
||||||
display: SearchDisplayFilters;
|
display: SearchDisplayFilters;
|
||||||
mediaType: MediaType;
|
mediaType: MediaType;
|
||||||
rating?: number;
|
rating?: number | null;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -276,6 +276,8 @@
|
|||||||
{#await getTagNames(value) then tagNames}
|
{#await getTagNames(value) then tagNames}
|
||||||
{tagNames}
|
{tagNames}
|
||||||
{/await}
|
{/await}
|
||||||
|
{:else if searchKey === 'rating'}
|
||||||
|
{$t('rating_count', { values: { count: value ?? 0 } })}
|
||||||
{:else if value === null || value === ''}
|
{:else if value === null || value === ''}
|
||||||
{$t('unknown')}
|
{$t('unknown')}
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
Reference in New Issue
Block a user