diff --git a/i18n/en.json b/i18n/en.json index 90beb7077e..0698fd7d10 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -565,6 +565,9 @@ "are_these_the_same_person": "Are these the same person?", "are_you_sure_to_do_this": "Are you sure you want to do this?", "array_field_not_fully_supported": "Array fields require manual JSON editing", + "aspect_ratio": "Aspect ratio", + "aspect_ratio_height": "Aspect ratio height", + "aspect_ratio_width": "Aspect ratio width", "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", "asset_added_to_album": "Added to album", @@ -1334,6 +1337,7 @@ "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1} and {person2} on {date}", "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1}, {person2}, and {person3} on {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1}, {person2}, and {additionalCount, number} others on {date}", + "image_properties": "Image properties", "image_saved_successfully": "Image saved", "image_viewer_page_state_provider_download_started": "Download Started", "image_viewer_page_state_provider_download_success": "Download Success", @@ -1592,7 +1596,13 @@ "merge_people_prompt": "Do you want to merge these people? This action is irreversible.", "merge_people_successfully": "Merge people successfully", "merged_people_count": "Merged {count, plural, one {# person} other {# people}}", + "max_aspect_ratio": "Max aspect ratio", + "max_height": "Max height", + "max_width": "Max width", "minimize": "Minimize", + "min_aspect_ratio": "Min aspect ratio", + "min_height": "Min height", + "min_width": "Min width", "minute": "Minute", "minutes": "Minutes", "mirror_horizontal": "Horizontal", diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 0118cabdba..97a77ef973 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -397,9 +397,27 @@ class SearchApi { /// * [String] make: /// Filter by camera make /// + /// * [num] maxAspectRatio: + /// Filter by maximum aspect ratio (width/height) + /// + /// * [int] maxHeight: + /// Filter by maximum image height + /// + /// * [int] maxWidth: + /// Filter by maximum image width + /// + /// * [num] minAspectRatio: + /// Filter by minimum aspect ratio (width/height) + /// /// * [int] minFileSize: /// Minimum file size in bytes /// + /// * [int] minHeight: + /// Filter by minimum image height + /// + /// * [int] minWidth: + /// Filter by minimum image width + /// /// * [String] model: /// Filter by camera model /// @@ -448,7 +466,7 @@ class SearchApi { /// /// * [bool] withExif: /// Include EXIF data in response - Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, int? rating, int? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, Future? abortTrigger, }) async { + Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, num? maxAspectRatio, int? maxHeight, int? maxWidth, num? minAspectRatio, int? minFileSize, int? minHeight, int? minWidth, String? model, String? ocr, List? personIds, int? rating, int? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, Future? abortTrigger, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/large-assets'; @@ -498,9 +516,27 @@ class SearchApi { if (make != null) { queryParams.addAll(_queryParams('', 'make', make)); } + if (maxAspectRatio != null) { + queryParams.addAll(_queryParams('', 'maxAspectRatio', maxAspectRatio)); + } + if (maxHeight != null) { + queryParams.addAll(_queryParams('', 'maxHeight', maxHeight)); + } + if (maxWidth != null) { + queryParams.addAll(_queryParams('', 'maxWidth', maxWidth)); + } + if (minAspectRatio != null) { + queryParams.addAll(_queryParams('', 'minAspectRatio', minAspectRatio)); + } if (minFileSize != null) { queryParams.addAll(_queryParams('', 'minFileSize', minFileSize)); } + if (minHeight != null) { + queryParams.addAll(_queryParams('', 'minHeight', minHeight)); + } + if (minWidth != null) { + queryParams.addAll(_queryParams('', 'minWidth', minWidth)); + } if (model != null) { queryParams.addAll(_queryParams('', 'model', model)); } @@ -613,9 +649,27 @@ class SearchApi { /// * [String] make: /// Filter by camera make /// + /// * [num] maxAspectRatio: + /// Filter by maximum aspect ratio (width/height) + /// + /// * [int] maxHeight: + /// Filter by maximum image height + /// + /// * [int] maxWidth: + /// Filter by maximum image width + /// + /// * [num] minAspectRatio: + /// Filter by minimum aspect ratio (width/height) + /// /// * [int] minFileSize: /// Minimum file size in bytes /// + /// * [int] minHeight: + /// Filter by minimum image height + /// + /// * [int] minWidth: + /// Filter by minimum image width + /// /// * [String] model: /// Filter by camera model /// @@ -664,8 +718,8 @@ class SearchApi { /// /// * [bool] withExif: /// Include EXIF data in response - Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, int? rating, int? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, Future? abortTrigger, }) async { - final response = await searchLargeAssetsWithHttpInfo(albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, abortTrigger: abortTrigger,); + Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, num? maxAspectRatio, int? maxHeight, int? maxWidth, num? minAspectRatio, int? minFileSize, int? minHeight, int? minWidth, String? model, String? ocr, List? personIds, int? rating, int? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, Future? abortTrigger, }) async { + final response = await searchLargeAssetsWithHttpInfo(albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, maxAspectRatio: maxAspectRatio, maxHeight: maxHeight, maxWidth: maxWidth, minAspectRatio: minAspectRatio, minFileSize: minFileSize, minHeight: minHeight, minWidth: minWidth, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 29b1d5b68d..cc98ec01e0 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -30,6 +30,12 @@ class MetadataSearchDto { this.lensModel, this.libraryId, this.make, + this.maxAspectRatio, + this.maxHeight, + this.maxWidth, + this.minAspectRatio, + this.minHeight, + this.minWidth, this.model, this.ocr, this.order, @@ -174,6 +180,76 @@ class MetadataSearchDto { /// Filter by camera make String? make; + /// Filter by maximum aspect ratio (width/height) + /// + /// Minimum value: 0 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? maxAspectRatio; + + /// Filter by maximum image height + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? maxHeight; + + /// Filter by maximum image width + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? maxWidth; + + /// Filter by minimum aspect ratio (width/height) + /// + /// Minimum value: 0 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? minAspectRatio; + + /// Filter by minimum image height + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? minHeight; + + /// Filter by minimum image width + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? minWidth; + /// Filter by camera model String? model; @@ -394,6 +470,12 @@ class MetadataSearchDto { other.lensModel == lensModel && other.libraryId == libraryId && other.make == make && + other.maxAspectRatio == maxAspectRatio && + other.maxHeight == maxHeight && + other.maxWidth == maxWidth && + other.minAspectRatio == minAspectRatio && + other.minHeight == minHeight && + other.minWidth == minWidth && other.model == model && other.ocr == ocr && other.order == order && @@ -440,6 +522,12 @@ class MetadataSearchDto { (lensModel == null ? 0 : lensModel!.hashCode) + (libraryId == null ? 0 : libraryId!.hashCode) + (make == null ? 0 : make!.hashCode) + + (maxAspectRatio == null ? 0 : maxAspectRatio!.hashCode) + + (maxHeight == null ? 0 : maxHeight!.hashCode) + + (maxWidth == null ? 0 : maxWidth!.hashCode) + + (minAspectRatio == null ? 0 : minAspectRatio!.hashCode) + + (minHeight == null ? 0 : minHeight!.hashCode) + + (minWidth == null ? 0 : minWidth!.hashCode) + (model == null ? 0 : model!.hashCode) + (ocr == null ? 0 : ocr!.hashCode) + (order == null ? 0 : order!.hashCode) + @@ -467,7 +555,7 @@ class MetadataSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, maxAspectRatio=$maxAspectRatio, maxHeight=$maxHeight, maxWidth=$maxWidth, minAspectRatio=$minAspectRatio, minHeight=$minHeight, minWidth=$minWidth, model=$model, ocr=$ocr, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -556,6 +644,36 @@ class MetadataSearchDto { } else { // json[r'make'] = null; } + if (this.maxAspectRatio != null) { + json[r'maxAspectRatio'] = this.maxAspectRatio; + } else { + // json[r'maxAspectRatio'] = null; + } + if (this.maxHeight != null) { + json[r'maxHeight'] = this.maxHeight; + } else { + // json[r'maxHeight'] = null; + } + if (this.maxWidth != null) { + json[r'maxWidth'] = this.maxWidth; + } else { + // json[r'maxWidth'] = null; + } + if (this.minAspectRatio != null) { + json[r'minAspectRatio'] = this.minAspectRatio; + } else { + // json[r'minAspectRatio'] = null; + } + if (this.minHeight != null) { + json[r'minHeight'] = this.minHeight; + } else { + // json[r'minHeight'] = null; + } + if (this.minWidth != null) { + json[r'minWidth'] = this.minWidth; + } else { + // json[r'minWidth'] = null; + } if (this.model != null) { json[r'model'] = this.model; } else { @@ -720,6 +838,12 @@ class MetadataSearchDto { lensModel: mapValueOfType(json, r'lensModel'), libraryId: mapValueOfType(json, r'libraryId'), make: mapValueOfType(json, r'make'), + maxAspectRatio: num.parse('${json[r'maxAspectRatio']}'), + maxHeight: mapValueOfType(json, r'maxHeight'), + maxWidth: mapValueOfType(json, r'maxWidth'), + minAspectRatio: num.parse('${json[r'minAspectRatio']}'), + minHeight: mapValueOfType(json, r'minHeight'), + minWidth: mapValueOfType(json, r'minWidth'), model: mapValueOfType(json, r'model'), ocr: mapValueOfType(json, r'ocr'), order: AssetOrder.fromJson(json[r'order']), diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart index 728072639c..725e4dee22 100644 --- a/mobile/openapi/lib/model/random_search_dto.dart +++ b/mobile/openapi/lib/model/random_search_dto.dart @@ -26,6 +26,12 @@ class RandomSearchDto { this.lensModel, this.libraryId, this.make, + this.maxAspectRatio, + this.maxHeight, + this.maxWidth, + this.minAspectRatio, + this.minHeight, + this.minWidth, this.model, this.ocr, this.personIds = const [], @@ -128,6 +134,76 @@ class RandomSearchDto { /// Filter by camera make String? make; + /// Filter by maximum aspect ratio (width/height) + /// + /// Minimum value: 0 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? maxAspectRatio; + + /// Filter by maximum image height + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? maxHeight; + + /// Filter by maximum image width + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? maxWidth; + + /// Filter by minimum aspect ratio (width/height) + /// + /// Minimum value: 0 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? minAspectRatio; + + /// Filter by minimum image height + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? minHeight; + + /// Filter by minimum image width + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? minWidth; + /// Filter by camera model String? model; @@ -288,6 +364,12 @@ class RandomSearchDto { other.lensModel == lensModel && other.libraryId == libraryId && other.make == make && + other.maxAspectRatio == maxAspectRatio && + other.maxHeight == maxHeight && + other.maxWidth == maxWidth && + other.minAspectRatio == minAspectRatio && + other.minHeight == minHeight && + other.minWidth == minWidth && other.model == model && other.ocr == ocr && _deepEquality.equals(other.personIds, personIds) && @@ -324,6 +406,12 @@ class RandomSearchDto { (lensModel == null ? 0 : lensModel!.hashCode) + (libraryId == null ? 0 : libraryId!.hashCode) + (make == null ? 0 : make!.hashCode) + + (maxAspectRatio == null ? 0 : maxAspectRatio!.hashCode) + + (maxHeight == null ? 0 : maxHeight!.hashCode) + + (maxWidth == null ? 0 : maxWidth!.hashCode) + + (minAspectRatio == null ? 0 : minAspectRatio!.hashCode) + + (minHeight == null ? 0 : minHeight!.hashCode) + + (minWidth == null ? 0 : minWidth!.hashCode) + (model == null ? 0 : model!.hashCode) + (ocr == null ? 0 : ocr!.hashCode) + (personIds.hashCode) + @@ -345,7 +433,7 @@ class RandomSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, maxAspectRatio=$maxAspectRatio, maxHeight=$maxHeight, maxWidth=$maxWidth, minAspectRatio=$minAspectRatio, minHeight=$minHeight, minWidth=$minWidth, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -414,6 +502,36 @@ class RandomSearchDto { } else { // json[r'make'] = null; } + if (this.maxAspectRatio != null) { + json[r'maxAspectRatio'] = this.maxAspectRatio; + } else { + // json[r'maxAspectRatio'] = null; + } + if (this.maxHeight != null) { + json[r'maxHeight'] = this.maxHeight; + } else { + // json[r'maxHeight'] = null; + } + if (this.maxWidth != null) { + json[r'maxWidth'] = this.maxWidth; + } else { + // json[r'maxWidth'] = null; + } + if (this.minAspectRatio != null) { + json[r'minAspectRatio'] = this.minAspectRatio; + } else { + // json[r'minAspectRatio'] = null; + } + if (this.minHeight != null) { + json[r'minHeight'] = this.minHeight; + } else { + // json[r'minHeight'] = null; + } + if (this.minWidth != null) { + json[r'minWidth'] = this.minWidth; + } else { + // json[r'minWidth'] = null; + } if (this.model != null) { json[r'model'] = this.model; } else { @@ -544,6 +662,12 @@ class RandomSearchDto { lensModel: mapValueOfType(json, r'lensModel'), libraryId: mapValueOfType(json, r'libraryId'), make: mapValueOfType(json, r'make'), + maxAspectRatio: num.parse('${json[r'maxAspectRatio']}'), + maxHeight: mapValueOfType(json, r'maxHeight'), + maxWidth: mapValueOfType(json, r'maxWidth'), + minAspectRatio: num.parse('${json[r'minAspectRatio']}'), + minHeight: mapValueOfType(json, r'minHeight'), + minWidth: mapValueOfType(json, r'minWidth'), model: mapValueOfType(json, r'model'), ocr: mapValueOfType(json, r'ocr'), personIds: json[r'personIds'] is Iterable diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 9bbb4a25f0..cc0d629398 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -27,6 +27,12 @@ class SmartSearchDto { this.lensModel, this.libraryId, this.make, + this.maxAspectRatio, + this.maxHeight, + this.maxWidth, + this.minAspectRatio, + this.minHeight, + this.minWidth, this.model, this.ocr, this.page, @@ -139,6 +145,76 @@ class SmartSearchDto { /// Filter by camera make String? make; + /// Filter by maximum aspect ratio (width/height) + /// + /// Minimum value: 0 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? maxAspectRatio; + + /// Filter by maximum image height + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? maxHeight; + + /// Filter by maximum image width + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? maxWidth; + + /// Filter by minimum aspect ratio (width/height) + /// + /// Minimum value: 0 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? minAspectRatio; + + /// Filter by minimum image height + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? minHeight; + + /// Filter by minimum image width + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? minWidth; + /// Filter by camera model String? model; @@ -312,6 +388,12 @@ class SmartSearchDto { other.lensModel == lensModel && other.libraryId == libraryId && other.make == make && + other.maxAspectRatio == maxAspectRatio && + other.maxHeight == maxHeight && + other.maxWidth == maxWidth && + other.minAspectRatio == minAspectRatio && + other.minHeight == minHeight && + other.minWidth == minWidth && other.model == model && other.ocr == ocr && other.page == page && @@ -350,6 +432,12 @@ class SmartSearchDto { (lensModel == null ? 0 : lensModel!.hashCode) + (libraryId == null ? 0 : libraryId!.hashCode) + (make == null ? 0 : make!.hashCode) + + (maxAspectRatio == null ? 0 : maxAspectRatio!.hashCode) + + (maxHeight == null ? 0 : maxHeight!.hashCode) + + (maxWidth == null ? 0 : maxWidth!.hashCode) + + (minAspectRatio == null ? 0 : minAspectRatio!.hashCode) + + (minHeight == null ? 0 : minHeight!.hashCode) + + (minWidth == null ? 0 : minWidth!.hashCode) + (model == null ? 0 : model!.hashCode) + (ocr == null ? 0 : ocr!.hashCode) + (page == null ? 0 : page!.hashCode) + @@ -372,7 +460,7 @@ class SmartSearchDto { (withExif == null ? 0 : withExif!.hashCode); @override - String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]'; + String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, maxAspectRatio=$maxAspectRatio, maxHeight=$maxHeight, maxWidth=$maxWidth, minAspectRatio=$minAspectRatio, minHeight=$minHeight, minWidth=$minWidth, model=$model, ocr=$ocr, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]'; Map toJson() { final json = {}; @@ -446,6 +534,36 @@ class SmartSearchDto { } else { // json[r'make'] = null; } + if (this.maxAspectRatio != null) { + json[r'maxAspectRatio'] = this.maxAspectRatio; + } else { + // json[r'maxAspectRatio'] = null; + } + if (this.maxHeight != null) { + json[r'maxHeight'] = this.maxHeight; + } else { + // json[r'maxHeight'] = null; + } + if (this.maxWidth != null) { + json[r'maxWidth'] = this.maxWidth; + } else { + // json[r'maxWidth'] = null; + } + if (this.minAspectRatio != null) { + json[r'minAspectRatio'] = this.minAspectRatio; + } else { + // json[r'minAspectRatio'] = null; + } + if (this.minHeight != null) { + json[r'minHeight'] = this.minHeight; + } else { + // json[r'minHeight'] = null; + } + if (this.minWidth != null) { + json[r'minWidth'] = this.minWidth; + } else { + // json[r'minWidth'] = null; + } if (this.model != null) { json[r'model'] = this.model; } else { @@ -582,6 +700,12 @@ class SmartSearchDto { lensModel: mapValueOfType(json, r'lensModel'), libraryId: mapValueOfType(json, r'libraryId'), make: mapValueOfType(json, r'make'), + maxAspectRatio: num.parse('${json[r'maxAspectRatio']}'), + maxHeight: mapValueOfType(json, r'maxHeight'), + maxWidth: mapValueOfType(json, r'maxWidth'), + minAspectRatio: num.parse('${json[r'minAspectRatio']}'), + minHeight: mapValueOfType(json, r'minHeight'), + minWidth: mapValueOfType(json, r'minWidth'), model: mapValueOfType(json, r'model'), ocr: mapValueOfType(json, r'ocr'), page: mapValueOfType(json, r'page'), diff --git a/mobile/openapi/lib/model/statistics_search_dto.dart b/mobile/openapi/lib/model/statistics_search_dto.dart index f276e3717b..6414985c26 100644 --- a/mobile/openapi/lib/model/statistics_search_dto.dart +++ b/mobile/openapi/lib/model/statistics_search_dto.dart @@ -27,6 +27,12 @@ class StatisticsSearchDto { this.lensModel, this.libraryId, this.make, + this.maxAspectRatio, + this.maxHeight, + this.maxWidth, + this.minAspectRatio, + this.minHeight, + this.minWidth, this.model, this.ocr, this.personIds = const [], @@ -133,6 +139,76 @@ class StatisticsSearchDto { /// Filter by camera make String? make; + /// Filter by maximum aspect ratio (width/height) + /// + /// Minimum value: 0 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? maxAspectRatio; + + /// Filter by maximum image height + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? maxHeight; + + /// Filter by maximum image width + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? maxWidth; + + /// Filter by minimum aspect ratio (width/height) + /// + /// Minimum value: 0 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? minAspectRatio; + + /// Filter by minimum image height + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? minHeight; + + /// Filter by minimum image width + /// + /// Minimum value: 1 + /// Maximum value: 9007199254740991 + /// + /// 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. + /// + int? minWidth; + /// Filter by camera model String? model; @@ -246,6 +322,12 @@ class StatisticsSearchDto { other.lensModel == lensModel && other.libraryId == libraryId && other.make == make && + other.maxAspectRatio == maxAspectRatio && + other.maxHeight == maxHeight && + other.maxWidth == maxWidth && + other.minAspectRatio == minAspectRatio && + other.minHeight == minHeight && + other.minWidth == minWidth && other.model == model && other.ocr == ocr && _deepEquality.equals(other.personIds, personIds) && @@ -278,6 +360,12 @@ class StatisticsSearchDto { (lensModel == null ? 0 : lensModel!.hashCode) + (libraryId == null ? 0 : libraryId!.hashCode) + (make == null ? 0 : make!.hashCode) + + (maxAspectRatio == null ? 0 : maxAspectRatio!.hashCode) + + (maxHeight == null ? 0 : maxHeight!.hashCode) + + (maxWidth == null ? 0 : maxWidth!.hashCode) + + (minAspectRatio == null ? 0 : minAspectRatio!.hashCode) + + (minHeight == null ? 0 : minHeight!.hashCode) + + (minWidth == null ? 0 : minWidth!.hashCode) + (model == null ? 0 : model!.hashCode) + (ocr == null ? 0 : ocr!.hashCode) + (personIds.hashCode) + @@ -294,7 +382,7 @@ class StatisticsSearchDto { (visibility == null ? 0 : visibility!.hashCode); @override - String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]'; + String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, maxAspectRatio=$maxAspectRatio, maxHeight=$maxHeight, maxWidth=$maxWidth, minAspectRatio=$minAspectRatio, minHeight=$minHeight, minWidth=$minWidth, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]'; Map toJson() { final json = {}; @@ -368,6 +456,36 @@ class StatisticsSearchDto { } else { // json[r'make'] = null; } + if (this.maxAspectRatio != null) { + json[r'maxAspectRatio'] = this.maxAspectRatio; + } else { + // json[r'maxAspectRatio'] = null; + } + if (this.maxHeight != null) { + json[r'maxHeight'] = this.maxHeight; + } else { + // json[r'maxHeight'] = null; + } + if (this.maxWidth != null) { + json[r'maxWidth'] = this.maxWidth; + } else { + // json[r'maxWidth'] = null; + } + if (this.minAspectRatio != null) { + json[r'minAspectRatio'] = this.minAspectRatio; + } else { + // json[r'minAspectRatio'] = null; + } + if (this.minHeight != null) { + json[r'minHeight'] = this.minHeight; + } else { + // json[r'minHeight'] = null; + } + if (this.minWidth != null) { + json[r'minWidth'] = this.minWidth; + } else { + // json[r'minWidth'] = null; + } if (this.model != null) { json[r'model'] = this.model; } else { @@ -474,6 +592,12 @@ class StatisticsSearchDto { lensModel: mapValueOfType(json, r'lensModel'), libraryId: mapValueOfType(json, r'libraryId'), make: mapValueOfType(json, r'make'), + maxAspectRatio: num.parse('${json[r'maxAspectRatio']}'), + maxHeight: mapValueOfType(json, r'maxHeight'), + maxWidth: mapValueOfType(json, r'maxWidth'), + minAspectRatio: num.parse('${json[r'minAspectRatio']}'), + minHeight: mapValueOfType(json, r'minHeight'), + minWidth: mapValueOfType(json, r'minWidth'), model: mapValueOfType(json, r'model'), ocr: mapValueOfType(json, r'ocr'), personIds: json[r'personIds'] is Iterable diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index d9087c375d..d34d60b9fa 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9823,6 +9823,50 @@ "nullable": true } }, + { + "name": "maxAspectRatio", + "required": false, + "in": "query", + "description": "Filter by maximum aspect ratio (width/height)", + "schema": { + "exclusiveMinimum": true, + "type": "number", + "minimum": 0 + } + }, + { + "name": "maxHeight", + "required": false, + "in": "query", + "description": "Filter by maximum image height", + "schema": { + "minimum": 1, + "maximum": 9007199254740991, + "type": "integer" + } + }, + { + "name": "maxWidth", + "required": false, + "in": "query", + "description": "Filter by maximum image width", + "schema": { + "minimum": 1, + "maximum": 9007199254740991, + "type": "integer" + } + }, + { + "name": "minAspectRatio", + "required": false, + "in": "query", + "description": "Filter by minimum aspect ratio (width/height)", + "schema": { + "exclusiveMinimum": true, + "type": "number", + "minimum": 0 + } + }, { "name": "minFileSize", "required": false, @@ -9834,6 +9878,28 @@ "type": "integer" } }, + { + "name": "minHeight", + "required": false, + "in": "query", + "description": "Filter by minimum image height", + "schema": { + "minimum": 1, + "maximum": 9007199254740991, + "type": "integer" + } + }, + { + "name": "minWidth", + "required": false, + "in": "query", + "description": "Filter by minimum image width", + "schema": { + "minimum": 1, + "maximum": 9007199254740991, + "type": "integer" + } + }, { "name": "model", "required": false, @@ -19324,6 +19390,42 @@ "nullable": true, "type": "string" }, + "maxAspectRatio": { + "description": "Filter by maximum aspect ratio (width/height)", + "exclusiveMinimum": true, + "minimum": 0, + "type": "number" + }, + "maxHeight": { + "description": "Filter by maximum image height", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "maxWidth": { + "description": "Filter by maximum image width", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "minAspectRatio": { + "description": "Filter by minimum aspect ratio (width/height)", + "exclusiveMinimum": true, + "minimum": 0, + "type": "number" + }, + "minHeight": { + "description": "Filter by minimum image height", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "minWidth": { + "description": "Filter by minimum image width", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, "model": { "description": "Filter by camera model", "nullable": true, @@ -21008,6 +21110,42 @@ "nullable": true, "type": "string" }, + "maxAspectRatio": { + "description": "Filter by maximum aspect ratio (width/height)", + "exclusiveMinimum": true, + "minimum": 0, + "type": "number" + }, + "maxHeight": { + "description": "Filter by maximum image height", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "maxWidth": { + "description": "Filter by maximum image width", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "minAspectRatio": { + "description": "Filter by minimum aspect ratio (width/height)", + "exclusiveMinimum": true, + "minimum": 0, + "type": "number" + }, + "minHeight": { + "description": "Filter by minimum image height", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "minWidth": { + "description": "Filter by minimum image width", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, "model": { "description": "Filter by camera model", "nullable": true, @@ -22453,6 +22591,42 @@ "nullable": true, "type": "string" }, + "maxAspectRatio": { + "description": "Filter by maximum aspect ratio (width/height)", + "exclusiveMinimum": true, + "minimum": 0, + "type": "number" + }, + "maxHeight": { + "description": "Filter by maximum image height", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "maxWidth": { + "description": "Filter by maximum image width", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "minAspectRatio": { + "description": "Filter by minimum aspect ratio (width/height)", + "exclusiveMinimum": true, + "minimum": 0, + "type": "number" + }, + "minHeight": { + "description": "Filter by minimum image height", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "minWidth": { + "description": "Filter by minimum image width", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, "model": { "description": "Filter by camera model", "nullable": true, @@ -22729,6 +22903,42 @@ "nullable": true, "type": "string" }, + "maxAspectRatio": { + "description": "Filter by maximum aspect ratio (width/height)", + "exclusiveMinimum": true, + "minimum": 0, + "type": "number" + }, + "maxHeight": { + "description": "Filter by maximum image height", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "maxWidth": { + "description": "Filter by maximum image width", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "minAspectRatio": { + "description": "Filter by minimum aspect ratio (width/height)", + "exclusiveMinimum": true, + "minimum": 0, + "type": "number" + }, + "minHeight": { + "description": "Filter by minimum image height", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "minWidth": { + "description": "Filter by minimum image width", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, "model": { "description": "Filter by camera model", "nullable": true, diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 163558e6a6..318f530dc1 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -1608,6 +1608,18 @@ export type MetadataSearchDto = { libraryId?: string | null; /** Filter by camera make */ make?: string | null; + /** Filter by maximum aspect ratio (width/height) */ + maxAspectRatio?: number; + /** Filter by maximum image height */ + maxHeight?: number; + /** Filter by maximum image width */ + maxWidth?: number; + /** Filter by minimum aspect ratio (width/height) */ + minAspectRatio?: number; + /** Filter by minimum image height */ + minHeight?: number; + /** Filter by minimum image width */ + minWidth?: number; /** Filter by camera model */ model?: string | null; /** Filter by OCR text content */ @@ -1729,6 +1741,18 @@ export type RandomSearchDto = { libraryId?: string | null; /** Filter by camera make */ make?: string | null; + /** Filter by maximum aspect ratio (width/height) */ + maxAspectRatio?: number; + /** Filter by maximum image height */ + maxHeight?: number; + /** Filter by maximum image width */ + maxWidth?: number; + /** Filter by minimum aspect ratio (width/height) */ + minAspectRatio?: number; + /** Filter by minimum image height */ + minHeight?: number; + /** Filter by minimum image width */ + minWidth?: number; /** Filter by camera model */ model?: string | null; /** Filter by OCR text content */ @@ -1795,6 +1819,18 @@ export type SmartSearchDto = { libraryId?: string | null; /** Filter by camera make */ make?: string | null; + /** Filter by maximum aspect ratio (width/height) */ + maxAspectRatio?: number; + /** Filter by maximum image height */ + maxHeight?: number; + /** Filter by maximum image width */ + maxWidth?: number; + /** Filter by minimum aspect ratio (width/height) */ + minAspectRatio?: number; + /** Filter by minimum image height */ + minHeight?: number; + /** Filter by minimum image width */ + minWidth?: number; /** Filter by camera model */ model?: string | null; /** Filter by OCR text content */ @@ -1863,6 +1899,18 @@ export type StatisticsSearchDto = { libraryId?: string | null; /** Filter by camera make */ make?: string | null; + /** Filter by maximum aspect ratio (width/height) */ + maxAspectRatio?: number; + /** Filter by maximum image height */ + maxHeight?: number; + /** Filter by maximum image width */ + maxWidth?: number; + /** Filter by minimum aspect ratio (width/height) */ + minAspectRatio?: number; + /** Filter by minimum image height */ + minHeight?: number; + /** Filter by minimum image width */ + minWidth?: number; /** Filter by camera model */ model?: string | null; /** Filter by OCR text content */ @@ -5484,7 +5532,7 @@ export function getExploreData(opts?: Oazapfts.RequestOpts) { /** * Search large assets */ -export function searchLargeAssets({ albumIds, city, country, createdAfter, createdBefore, isEncoded, isFavorite, isMotion, isNotInAlbum, isOffline, lensModel, libraryId, make, minFileSize, model, ocr, personIds, rating, size, state, tagIds, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, visibility, withDeleted, withExif }: { +export function searchLargeAssets({ albumIds, city, country, createdAfter, createdBefore, isEncoded, isFavorite, isMotion, isNotInAlbum, isOffline, lensModel, libraryId, make, maxAspectRatio, maxHeight, maxWidth, minAspectRatio, minFileSize, minHeight, minWidth, model, ocr, personIds, rating, size, state, tagIds, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, visibility, withDeleted, withExif }: { albumIds?: string[]; city?: string | null; country?: string | null; @@ -5498,7 +5546,13 @@ export function searchLargeAssets({ albumIds, city, country, createdAfter, creat lensModel?: string | null; libraryId?: string | null; make?: string | null; + maxAspectRatio?: number; + maxHeight?: number; + maxWidth?: number; + minAspectRatio?: number; minFileSize?: number; + minHeight?: number; + minWidth?: number; model?: string | null; ocr?: string; personIds?: string[]; @@ -5534,7 +5588,13 @@ export function searchLargeAssets({ albumIds, city, country, createdAfter, creat lensModel, libraryId, make, + maxAspectRatio, + maxHeight, + maxWidth, + minAspectRatio, minFileSize, + minHeight, + minWidth, model, ocr, personIds, diff --git a/server/src/controllers/search.controller.spec.ts b/server/src/controllers/search.controller.spec.ts index a1fed4c7ae..4508c6d031 100644 --- a/server/src/controllers/search.controller.spec.ts +++ b/server/src/controllers/search.controller.spec.ts @@ -120,6 +120,42 @@ describe(SearchController.name, () => { ); }); + it('should reject minWidth as a negative number', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ minWidth: -1 }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([{ path: ['minWidth'], message: 'Too small: expected number to be >=1' }]), + ); + }); + + it('should reject minAspectRatio as zero', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ minAspectRatio: 0 }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([{ path: ['minAspectRatio'], message: expect.stringContaining('Too small') }]), + ); + }); + + it('should reject maxAspectRatio as zero', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ maxAspectRatio: 0 }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([{ path: ['maxAspectRatio'], message: expect.stringContaining('Too small') }]), + ); + }); + + it('should reject minAspectRatio as a string', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/metadata') + .send({ minAspectRatio: 'abc' }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([ + { path: ['minAspectRatio'], message: 'Invalid input: expected number, received NaN' }, + ]), + ); + }); + describe('POST /search/random', () => { it('should be an authenticated route', async () => { await request(ctx.getHttpServer()).post('/search/random'); diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index c9a92b165f..00117f5316 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -29,6 +29,12 @@ const BaseSearchSchema = z.object({ make: emptyStringToNull(z.string().nullable()).optional().describe('Filter by camera make'), model: emptyStringToNull(z.string().nullable()).optional().describe('Filter by camera model'), lensModel: emptyStringToNull(z.string().nullable()).optional().describe('Filter by lens model'), + minAspectRatio: z.coerce.number().positive().optional().describe('Filter by minimum aspect ratio (width/height)'), + maxAspectRatio: z.coerce.number().positive().optional().describe('Filter by maximum aspect ratio (width/height)'), + minWidth: z.coerce.number().int().min(1).optional().describe('Filter by minimum image width'), + maxWidth: z.coerce.number().int().min(1).optional().describe('Filter by maximum image width'), + minHeight: z.coerce.number().int().min(1).optional().describe('Filter by minimum image height'), + maxHeight: z.coerce.number().int().min(1).optional().describe('Filter by maximum image height'), isNotInAlbum: z.boolean().optional().describe('Filter assets not in any album'), personIds: z.array(z.uuidv4()).optional().describe('Filter by person IDs'), tagIds: z.array(z.uuidv4()).nullish().describe('Filter by tag IDs'), diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 6f03c80ce1..3a13c919fc 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -69,6 +69,12 @@ export interface SearchExifOptions { country?: string | null; lensModel?: string | null; make?: string | null; + maxAspectRatio?: number; + maxHeight?: number; + maxWidth?: number; + minAspectRatio?: number; + minHeight?: number; + minWidth?: number; model?: string | null; state?: string | null; description?: string | null; diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index cb942b5366..e1c7bf02e1 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -428,6 +428,16 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') .where('asset_exif.rating', options.rating === null ? 'is' : '=', options.rating!), ) + .$if(options.minAspectRatio !== undefined, (qb) => + qb.where(sql`asset.width::double precision / nullif(asset.height, 0)`, '>=', options.minAspectRatio!), + ) + .$if(options.maxAspectRatio !== undefined, (qb) => + qb.where(sql`asset.width::double precision / nullif(asset.height, 0)`, '<=', options.maxAspectRatio!), + ) + .$if(options.minWidth !== undefined, (qb) => qb.where('asset.width', '>=', options.minWidth!)) + .$if(options.maxWidth !== undefined, (qb) => qb.where('asset.width', '<=', options.maxWidth!)) + .$if(options.minHeight !== undefined, (qb) => qb.where('asset.height', '>=', options.minHeight!)) + .$if(options.maxHeight !== undefined, (qb) => qb.where('asset.height', '<=', options.maxHeight!)) .$if(!!options.checksum, (qb) => qb.where('asset.checksum', '=', options.checksum!)) .$if(!!options.id, (qb) => qb.where('asset.id', '=', asUuid(options.id!))) .$if(!!options.libraryId, (qb) => qb.where('asset.libraryId', '=', asUuid(options.libraryId!))) diff --git a/server/test/medium/specs/services/search.service.spec.ts b/server/test/medium/specs/services/search.service.spec.ts index 18e03b2e48..59256f8658 100644 --- a/server/test/medium/specs/services/search.service.spec.ts +++ b/server/test/medium/specs/services/search.service.spec.ts @@ -110,6 +110,83 @@ describe(SearchService.name, () => { }); }); + describe('aspect ratio filter', () => { + it('should filter by minAspectRatio', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + + const { asset: wideAsset } = await ctx.newAsset({ ownerId: user.id, width: 4000, height: 1000 }); + await ctx.newAsset({ ownerId: user.id, width: 3000, height: 3000 }); + await ctx.newAsset({ ownerId: user.id, width: 1000, height: 4000 }); + + const auth = factory.auth({ user: { id: user.id } }); + const response = await sut.searchMetadata(auth, { minAspectRatio: 2 }); + + expect(response.assets.items).toEqual([expect.objectContaining({ id: wideAsset.id })]); + }); + + it('should filter by maxAspectRatio', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + + await ctx.newAsset({ ownerId: user.id, width: 4000, height: 3000 }); + const { asset: narrowAsset } = await ctx.newAsset({ ownerId: user.id, width: 1000, height: 4000 }); + await ctx.newAsset({ ownerId: user.id, width: 3000, height: 3000 }); + + const auth = factory.auth({ user: { id: user.id } }); + const response = await sut.searchMetadata(auth, { maxAspectRatio: 0.5 }); + + expect(response.assets.items).toEqual([expect.objectContaining({ id: narrowAsset.id })]); + }); + + it('should filter by both minAspectRatio and maxAspectRatio', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + + // ratio 4:3 = 1.333 — within [1.2, 1.5] + const { asset: inRangeAsset } = await ctx.newAsset({ ownerId: user.id, width: 4000, height: 3000 }); + // ratio 4:1 = 4.0 — above max + await ctx.newAsset({ ownerId: user.id, width: 4000, height: 1000 }); + // ratio 1:4 = 0.25 — below min + await ctx.newAsset({ ownerId: user.id, width: 1000, height: 4000 }); + + const auth = factory.auth({ user: { id: user.id } }); + const response = await sut.searchMetadata(auth, { minAspectRatio: 1.2, maxAspectRatio: 1.5 }); + + expect(response.assets.items).toEqual([expect.objectContaining({ id: inRangeAsset.id })]); + }); + }); + + describe('resolution filter', () => { + it('should filter by minimum width and height', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + + const { asset: highResAsset } = await ctx.newAsset({ ownerId: user.id, width: 4000, height: 3000 }); + await ctx.newAsset({ ownerId: user.id, width: 3000, height: 3000 }); + await ctx.newAsset({ ownerId: user.id, width: 4000, height: 2000 }); + + const auth = factory.auth({ user: { id: user.id } }); + const response = await sut.searchMetadata(auth, { minWidth: 3500, minHeight: 2500 }); + + expect(response.assets.items).toEqual([expect.objectContaining({ id: highResAsset.id })]); + }); + + it('should filter by maximum width and height', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + + await ctx.newAsset({ ownerId: user.id, width: 2000, height: 1500 }); + const { asset: lowResAsset } = await ctx.newAsset({ ownerId: user.id, width: 1000, height: 800 }); + await ctx.newAsset({ ownerId: user.id, width: 1200, height: 1600 }); + + const auth = factory.auth({ user: { id: user.id } }); + const response = await sut.searchMetadata(auth, { maxWidth: 1200, maxHeight: 1000 }); + + expect(response.assets.items).toEqual([expect.objectContaining({ id: lowResAsset.id })]); + }); + }); + describe('getSearchSuggestions', () => { it('should filter out empty search suggestions', async () => { const { sut, ctx } = setup(); diff --git a/web/src/lib/components/shared-components/search-bar/SearchImagePropsSection.svelte b/web/src/lib/components/shared-components/search-bar/SearchImagePropsSection.svelte new file mode 100644 index 0000000000..a7354b3f90 --- /dev/null +++ b/web/src/lib/components/shared-components/search-bar/SearchImagePropsSection.svelte @@ -0,0 +1,79 @@ + + +
+ {$t('image_properties')} + +
+
+ {$t('aspect_ratio')} +
+ +
+ + +
+ +
+ + +
+
+
+ +
+ {$t('resolution')} +
+ +
+ + +
+ +
+ + +
+
+
+
+
diff --git a/web/src/lib/modals/SearchFilterModal.svelte b/web/src/lib/modals/SearchFilterModal.svelte index 0145caacaf..9430e45262 100644 --- a/web/src/lib/modals/SearchFilterModal.svelte +++ b/web/src/lib/modals/SearchFilterModal.svelte @@ -8,6 +8,7 @@ import SearchRatingsSection from '$lib/components/shared-components/search-bar/SearchRatingsSection.svelte'; import SearchTagsSection from '$lib/components/shared-components/search-bar/SearchTagsSection.svelte'; import SearchTextSection from '$lib/components/shared-components/search-bar/SearchTextSection.svelte'; + import SearchImagePropsSection from '$lib/components/shared-components/search-bar/SearchImagePropsSection.svelte'; import { MediaType, QueryType, validQueryTypes } from '$lib/constants'; import { authManager } from '$lib/managers/auth-manager.svelte'; import type { SearchFilter } from '$lib/types'; @@ -28,6 +29,73 @@ let { searchQuery, onClose }: Props = $props(); const parseOptionalDate = (dateString?: DateTime) => (dateString ? parseUtcDate(dateString.toString()) : undefined); + + const numberToOptionalString = (value?: number | null) => + value === null || value === undefined ? undefined : value.toString(); + + const parseOptionalPositiveInteger = (value?: string) => { + if (!value?.trim()) { + return undefined; + } + + const trimmed = value.trim(); + const parsed = Number(trimmed); + return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined; + }; + + const greatestCommonDivisor = (left: number, right: number): number => { + let a = Math.abs(left); + let b = Math.abs(right); + + while (b !== 0) { + [a, b] = [b, a % b]; + } + + return a || 1; + }; + + const ratioToOptionalPair = (value?: number | null) => { + if (value === null || value === undefined || !Number.isFinite(value) || value <= 0) { + return { width: undefined, height: undefined }; + } + + let bestWidth = 1; + let bestHeight = 1; + let bestError = Number.POSITIVE_INFINITY; + + for (let denominator = 1; denominator <= 1000; denominator++) { + const numerator = Math.max(1, Math.round(value * denominator)); + const error = Math.abs(numerator / denominator - value); + + if (error < bestError) { + bestWidth = numerator; + bestHeight = denominator; + bestError = error; + } + + if (error < Number.EPSILON) { + break; + } + } + + const divisor = greatestCommonDivisor(bestWidth, bestHeight); + return { + width: numberToOptionalString(bestWidth / divisor), + height: numberToOptionalString(bestHeight / divisor), + }; + }; + + const parseOptionalAspectRatio = (width?: string, height?: string) => { + const parsedWidth = parseOptionalPositiveInteger(width); + const parsedHeight = parseOptionalPositiveInteger(height); + + if (!parsedWidth || !parsedHeight) { + return undefined; + } + + return parsedWidth / parsedHeight; + }; + const toStartOfDayDate = (dateString: string) => parseUtcDate(dateString)?.startOf('day') || undefined; const formId = generateId(); @@ -58,6 +126,9 @@ query = searchQuery.originalPath; } + const minAspectRatio = ratioToOptionalPair(searchQuery.minAspectRatio); + const maxAspectRatio = ratioToOptionalPair(searchQuery.maxAspectRatio); + return { query, ocr: searchQuery.ocr, @@ -96,6 +167,16 @@ ? MediaType.Video : MediaType.All, rating: searchQuery.rating, + imageProperties: { + minAspectRatioWidth: minAspectRatio.width, + minAspectRatioHeight: minAspectRatio.height, + maxAspectRatioWidth: maxAspectRatio.width, + maxAspectRatioHeight: maxAspectRatio.height, + minWidth: numberToOptionalString(searchQuery.minWidth), + maxWidth: numberToOptionalString(searchQuery.maxWidth), + minHeight: numberToOptionalString(searchQuery.minHeight), + maxHeight: numberToOptionalString(searchQuery.maxHeight), + }, }; }; @@ -118,6 +199,16 @@ }, mediaType: MediaType.All, rating: undefined, + imageProperties: { + minAspectRatioWidth: undefined, + minAspectRatioHeight: undefined, + maxAspectRatioWidth: undefined, + maxAspectRatioHeight: undefined, + minWidth: undefined, + maxWidth: undefined, + minHeight: undefined, + maxHeight: undefined, + }, }; }; @@ -149,6 +240,18 @@ visibility: filter.display.isArchive ? AssetVisibility.Archive : undefined, isFavorite: filter.display.isFavorite || undefined, isNotInAlbum: filter.display.isNotInAlbum || undefined, + minAspectRatio: parseOptionalAspectRatio( + filter.imageProperties.minAspectRatioWidth, + filter.imageProperties.minAspectRatioHeight, + ), + maxAspectRatio: parseOptionalAspectRatio( + filter.imageProperties.maxAspectRatioWidth, + filter.imageProperties.maxAspectRatioHeight, + ), + minWidth: parseOptionalPositiveInteger(filter.imageProperties.minWidth), + maxWidth: parseOptionalPositiveInteger(filter.imageProperties.maxWidth), + minHeight: parseOptionalPositiveInteger(filter.imageProperties.minHeight), + maxHeight: parseOptionalPositiveInteger(filter.imageProperties.maxHeight), personIds: filter.personIds.size > 0 ? [...filter.personIds] : undefined, tagIds: filter.tagIds === null ? null : filter.tagIds.size > 0 ? [...filter.tagIds] : undefined, type, @@ -209,6 +312,9 @@ + + + diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 41d98df097..808f5d71d0 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -63,6 +63,17 @@ export type SearchDisplayFilters = { isFavorite: boolean; }; +export type SearchImagePropsFilter = { + minAspectRatioWidth?: string; + minAspectRatioHeight?: string; + maxAspectRatioWidth?: string; + maxAspectRatioHeight?: string; + minWidth?: string; + maxWidth?: string; + minHeight?: string; + maxHeight?: string; +}; + export type SearchLocationFilter = { country?: string; state?: string; @@ -82,6 +93,7 @@ export type SearchFilter = { display: SearchDisplayFilters; mediaType: MediaType; rating?: number | null; + imageProperties: SearchImagePropsFilter; }; export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'object'; diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index 86a3040d5a..eb48b90d05 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -189,6 +189,12 @@ description: $t('description'), queryAssetId: $t('query_asset_id'), ocr: $t('ocr'), + minAspectRatio: $t('min_aspect_ratio'), + maxAspectRatio: $t('max_aspect_ratio'), + minWidth: $t('min_width'), + maxWidth: $t('max_width'), + minHeight: $t('min_height'), + maxHeight: $t('max_height'), }; return keyMap[key] || key; }