mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-30 18:22:37 -04:00 
			
		
		
		
	feat: add searching by tags (#15395)
* feat: add searching by tags * fix: fix merge --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									221e197633
								
							
						
					
					
						commit
						9ac95d6845
					
				
							
								
								
									
										11
									
								
								mobile/openapi/lib/model/metadata_search_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								mobile/openapi/lib/model/metadata_search_dto.dart
									
									
									
										generated
									
									
									
								
							| @ -41,6 +41,7 @@ class MetadataSearchDto { | |||||||
|     this.previewPath, |     this.previewPath, | ||||||
|     this.size, |     this.size, | ||||||
|     this.state, |     this.state, | ||||||
|  |     this.tagIds = const [], | ||||||
|     this.takenAfter, |     this.takenAfter, | ||||||
|     this.takenBefore, |     this.takenBefore, | ||||||
|     this.thumbnailPath, |     this.thumbnailPath, | ||||||
| @ -235,6 +236,8 @@ class MetadataSearchDto { | |||||||
| 
 | 
 | ||||||
|   String? state; |   String? state; | ||||||
| 
 | 
 | ||||||
|  |   List<String> tagIds; | ||||||
|  | 
 | ||||||
|   /// |   /// | ||||||
|   /// Please note: This property should have been non-nullable! Since the specification file |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|   /// does not include a default value (using the "default:" property), however, the generated |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
| @ -363,6 +366,7 @@ class MetadataSearchDto { | |||||||
|     other.previewPath == previewPath && |     other.previewPath == previewPath && | ||||||
|     other.size == size && |     other.size == size && | ||||||
|     other.state == state && |     other.state == state && | ||||||
|  |     _deepEquality.equals(other.tagIds, tagIds) && | ||||||
|     other.takenAfter == takenAfter && |     other.takenAfter == takenAfter && | ||||||
|     other.takenBefore == takenBefore && |     other.takenBefore == takenBefore && | ||||||
|     other.thumbnailPath == thumbnailPath && |     other.thumbnailPath == thumbnailPath && | ||||||
| @ -408,6 +412,7 @@ class MetadataSearchDto { | |||||||
|     (previewPath == null ? 0 : previewPath!.hashCode) + |     (previewPath == null ? 0 : previewPath!.hashCode) + | ||||||
|     (size == null ? 0 : size!.hashCode) + |     (size == null ? 0 : size!.hashCode) + | ||||||
|     (state == null ? 0 : state!.hashCode) + |     (state == null ? 0 : state!.hashCode) + | ||||||
|  |     (tagIds.hashCode) + | ||||||
|     (takenAfter == null ? 0 : takenAfter!.hashCode) + |     (takenAfter == null ? 0 : takenAfter!.hashCode) + | ||||||
|     (takenBefore == null ? 0 : takenBefore!.hashCode) + |     (takenBefore == null ? 0 : takenBefore!.hashCode) + | ||||||
|     (thumbnailPath == null ? 0 : thumbnailPath!.hashCode) + |     (thumbnailPath == null ? 0 : thumbnailPath!.hashCode) + | ||||||
| @ -423,7 +428,7 @@ class MetadataSearchDto { | |||||||
|     (withStacked == null ? 0 : withStacked!.hashCode); |     (withStacked == null ? 0 : withStacked!.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; |   String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @ -559,6 +564,7 @@ class MetadataSearchDto { | |||||||
|     } else { |     } else { | ||||||
|     //  json[r'state'] = null; |     //  json[r'state'] = null; | ||||||
|     } |     } | ||||||
|  |       json[r'tagIds'] = this.tagIds; | ||||||
|     if (this.takenAfter != null) { |     if (this.takenAfter != null) { | ||||||
|       json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); |       json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); | ||||||
|     } else { |     } else { | ||||||
| @ -662,6 +668,9 @@ class MetadataSearchDto { | |||||||
|         previewPath: mapValueOfType<String>(json, r'previewPath'), |         previewPath: mapValueOfType<String>(json, r'previewPath'), | ||||||
|         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 | ||||||
|  |             ? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false) | ||||||
|  |             : const [], | ||||||
|         takenAfter: mapDateTime(json, r'takenAfter', r''), |         takenAfter: mapDateTime(json, r'takenAfter', r''), | ||||||
|         takenBefore: mapDateTime(json, r'takenBefore', r''), |         takenBefore: mapDateTime(json, r'takenBefore', r''), | ||||||
|         thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath'), |         thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath'), | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								mobile/openapi/lib/model/random_search_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								mobile/openapi/lib/model/random_search_dto.dart
									
									
									
										generated
									
									
									
								
							| @ -32,6 +32,7 @@ class RandomSearchDto { | |||||||
|     this.personIds = const [], |     this.personIds = const [], | ||||||
|     this.size, |     this.size, | ||||||
|     this.state, |     this.state, | ||||||
|  |     this.tagIds = const [], | ||||||
|     this.takenAfter, |     this.takenAfter, | ||||||
|     this.takenBefore, |     this.takenBefore, | ||||||
|     this.trashedAfter, |     this.trashedAfter, | ||||||
| @ -158,6 +159,8 @@ class RandomSearchDto { | |||||||
| 
 | 
 | ||||||
|   String? state; |   String? state; | ||||||
| 
 | 
 | ||||||
|  |   List<String> tagIds; | ||||||
|  | 
 | ||||||
|   /// |   /// | ||||||
|   /// Please note: This property should have been non-nullable! Since the specification file |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|   /// does not include a default value (using the "default:" property), however, the generated |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
| @ -269,6 +272,7 @@ class RandomSearchDto { | |||||||
|     _deepEquality.equals(other.personIds, personIds) && |     _deepEquality.equals(other.personIds, personIds) && | ||||||
|     other.size == size && |     other.size == size && | ||||||
|     other.state == state && |     other.state == state && | ||||||
|  |     _deepEquality.equals(other.tagIds, tagIds) && | ||||||
|     other.takenAfter == takenAfter && |     other.takenAfter == takenAfter && | ||||||
|     other.takenBefore == takenBefore && |     other.takenBefore == takenBefore && | ||||||
|     other.trashedAfter == trashedAfter && |     other.trashedAfter == trashedAfter && | ||||||
| @ -304,6 +308,7 @@ class RandomSearchDto { | |||||||
|     (personIds.hashCode) + |     (personIds.hashCode) + | ||||||
|     (size == null ? 0 : size!.hashCode) + |     (size == null ? 0 : size!.hashCode) + | ||||||
|     (state == null ? 0 : state!.hashCode) + |     (state == null ? 0 : state!.hashCode) + | ||||||
|  |     (tagIds.hashCode) + | ||||||
|     (takenAfter == null ? 0 : takenAfter!.hashCode) + |     (takenAfter == null ? 0 : takenAfter!.hashCode) + | ||||||
|     (takenBefore == null ? 0 : takenBefore!.hashCode) + |     (takenBefore == null ? 0 : takenBefore!.hashCode) + | ||||||
|     (trashedAfter == null ? 0 : trashedAfter!.hashCode) + |     (trashedAfter == null ? 0 : trashedAfter!.hashCode) + | ||||||
| @ -318,7 +323,7 @@ class RandomSearchDto { | |||||||
|     (withStacked == null ? 0 : withStacked!.hashCode); |     (withStacked == null ? 0 : withStacked!.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; |   String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @ -413,6 +418,7 @@ class RandomSearchDto { | |||||||
|     } else { |     } else { | ||||||
|     //  json[r'state'] = null; |     //  json[r'state'] = null; | ||||||
|     } |     } | ||||||
|  |       json[r'tagIds'] = this.tagIds; | ||||||
|     if (this.takenAfter != null) { |     if (this.takenAfter != null) { | ||||||
|       json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); |       json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); | ||||||
|     } else { |     } else { | ||||||
| @ -502,6 +508,9 @@ class RandomSearchDto { | |||||||
|             : const [], |             : const [], | ||||||
|         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 | ||||||
|  |             ? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false) | ||||||
|  |             : const [], | ||||||
|         takenAfter: mapDateTime(json, r'takenAfter', r''), |         takenAfter: mapDateTime(json, r'takenAfter', r''), | ||||||
|         takenBefore: mapDateTime(json, r'takenBefore', r''), |         takenBefore: mapDateTime(json, r'takenBefore', r''), | ||||||
|         trashedAfter: mapDateTime(json, r'trashedAfter', r''), |         trashedAfter: mapDateTime(json, r'trashedAfter', r''), | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								mobile/openapi/lib/model/smart_search_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								mobile/openapi/lib/model/smart_search_dto.dart
									
									
									
										generated
									
									
									
								
							| @ -34,6 +34,7 @@ class SmartSearchDto { | |||||||
|     required this.query, |     required this.query, | ||||||
|     this.size, |     this.size, | ||||||
|     this.state, |     this.state, | ||||||
|  |     this.tagIds = const [], | ||||||
|     this.takenAfter, |     this.takenAfter, | ||||||
|     this.takenBefore, |     this.takenBefore, | ||||||
|     this.trashedAfter, |     this.trashedAfter, | ||||||
| @ -169,6 +170,8 @@ class SmartSearchDto { | |||||||
| 
 | 
 | ||||||
|   String? state; |   String? state; | ||||||
| 
 | 
 | ||||||
|  |   List<String> tagIds; | ||||||
|  | 
 | ||||||
|   /// |   /// | ||||||
|   /// Please note: This property should have been non-nullable! Since the specification file |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|   /// does not include a default value (using the "default:" property), however, the generated |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
| @ -266,6 +269,7 @@ class SmartSearchDto { | |||||||
|     other.query == query && |     other.query == query && | ||||||
|     other.size == size && |     other.size == size && | ||||||
|     other.state == state && |     other.state == state && | ||||||
|  |     _deepEquality.equals(other.tagIds, tagIds) && | ||||||
|     other.takenAfter == takenAfter && |     other.takenAfter == takenAfter && | ||||||
|     other.takenBefore == takenBefore && |     other.takenBefore == takenBefore && | ||||||
|     other.trashedAfter == trashedAfter && |     other.trashedAfter == trashedAfter && | ||||||
| @ -301,6 +305,7 @@ class SmartSearchDto { | |||||||
|     (query.hashCode) + |     (query.hashCode) + | ||||||
|     (size == null ? 0 : size!.hashCode) + |     (size == null ? 0 : size!.hashCode) + | ||||||
|     (state == null ? 0 : state!.hashCode) + |     (state == null ? 0 : state!.hashCode) + | ||||||
|  |     (tagIds.hashCode) + | ||||||
|     (takenAfter == null ? 0 : takenAfter!.hashCode) + |     (takenAfter == null ? 0 : takenAfter!.hashCode) + | ||||||
|     (takenBefore == null ? 0 : takenBefore!.hashCode) + |     (takenBefore == null ? 0 : takenBefore!.hashCode) + | ||||||
|     (trashedAfter == null ? 0 : trashedAfter!.hashCode) + |     (trashedAfter == null ? 0 : trashedAfter!.hashCode) + | ||||||
| @ -313,7 +318,7 @@ class SmartSearchDto { | |||||||
|     (withExif == null ? 0 : withExif!.hashCode); |     (withExif == null ? 0 : withExif!.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]'; |   String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @ -414,6 +419,7 @@ class SmartSearchDto { | |||||||
|     } else { |     } else { | ||||||
|     //  json[r'state'] = null; |     //  json[r'state'] = null; | ||||||
|     } |     } | ||||||
|  |       json[r'tagIds'] = this.tagIds; | ||||||
|     if (this.takenAfter != null) { |     if (this.takenAfter != null) { | ||||||
|       json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); |       json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); | ||||||
|     } else { |     } else { | ||||||
| @ -495,6 +501,9 @@ class SmartSearchDto { | |||||||
|         query: mapValueOfType<String>(json, r'query')!, |         query: mapValueOfType<String>(json, r'query')!, | ||||||
|         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 | ||||||
|  |             ? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false) | ||||||
|  |             : const [], | ||||||
|         takenAfter: mapDateTime(json, r'takenAfter', r''), |         takenAfter: mapDateTime(json, r'takenAfter', r''), | ||||||
|         takenBefore: mapDateTime(json, r'takenBefore', r''), |         takenBefore: mapDateTime(json, r'takenBefore', r''), | ||||||
|         trashedAfter: mapDateTime(json, r'trashedAfter', r''), |         trashedAfter: mapDateTime(json, r'trashedAfter', r''), | ||||||
|  | |||||||
| @ -10036,6 +10036,13 @@ | |||||||
|             "nullable": true, |             "nullable": true, | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           }, |           }, | ||||||
|  |           "tagIds": { | ||||||
|  |             "items": { | ||||||
|  |               "format": "uuid", | ||||||
|  |               "type": "string" | ||||||
|  |             }, | ||||||
|  |             "type": "array" | ||||||
|  |           }, | ||||||
|           "takenAfter": { |           "takenAfter": { | ||||||
|             "format": "date-time", |             "format": "date-time", | ||||||
|             "type": "string" |             "type": "string" | ||||||
| @ -10649,6 +10656,13 @@ | |||||||
|             "nullable": true, |             "nullable": true, | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           }, |           }, | ||||||
|  |           "tagIds": { | ||||||
|  |             "items": { | ||||||
|  |               "format": "uuid", | ||||||
|  |               "type": "string" | ||||||
|  |             }, | ||||||
|  |             "type": "array" | ||||||
|  |           }, | ||||||
|           "takenAfter": { |           "takenAfter": { | ||||||
|             "format": "date-time", |             "format": "date-time", | ||||||
|             "type": "string" |             "type": "string" | ||||||
| @ -11564,6 +11578,13 @@ | |||||||
|             "nullable": true, |             "nullable": true, | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           }, |           }, | ||||||
|  |           "tagIds": { | ||||||
|  |             "items": { | ||||||
|  |               "format": "uuid", | ||||||
|  |               "type": "string" | ||||||
|  |             }, | ||||||
|  |             "type": "array" | ||||||
|  |           }, | ||||||
|           "takenAfter": { |           "takenAfter": { | ||||||
|             "format": "date-time", |             "format": "date-time", | ||||||
|             "type": "string" |             "type": "string" | ||||||
|  | |||||||
| @ -792,6 +792,7 @@ export type MetadataSearchDto = { | |||||||
|     previewPath?: string; |     previewPath?: string; | ||||||
|     size?: number; |     size?: number; | ||||||
|     state?: string | null; |     state?: string | null; | ||||||
|  |     tagIds?: string[]; | ||||||
|     takenAfter?: string; |     takenAfter?: string; | ||||||
|     takenBefore?: string; |     takenBefore?: string; | ||||||
|     thumbnailPath?: string; |     thumbnailPath?: string; | ||||||
| @ -858,6 +859,7 @@ export type RandomSearchDto = { | |||||||
|     personIds?: string[]; |     personIds?: string[]; | ||||||
|     size?: number; |     size?: number; | ||||||
|     state?: string | null; |     state?: string | null; | ||||||
|  |     tagIds?: string[]; | ||||||
|     takenAfter?: string; |     takenAfter?: string; | ||||||
|     takenBefore?: string; |     takenBefore?: string; | ||||||
|     trashedAfter?: string; |     trashedAfter?: string; | ||||||
| @ -893,6 +895,7 @@ export type SmartSearchDto = { | |||||||
|     query: string; |     query: string; | ||||||
|     size?: number; |     size?: number; | ||||||
|     state?: string | null; |     state?: string | null; | ||||||
|  |     tagIds?: string[]; | ||||||
|     takenAfter?: string; |     takenAfter?: string; | ||||||
|     takenBefore?: string; |     takenBefore?: string; | ||||||
|     trashedAfter?: string; |     trashedAfter?: string; | ||||||
|  | |||||||
| @ -111,6 +111,9 @@ class BaseSearchDto { | |||||||
| 
 | 
 | ||||||
|   @ValidateUUID({ each: true, optional: true }) |   @ValidateUUID({ each: true, optional: true }) | ||||||
|   personIds?: string[]; |   personIds?: string[]; | ||||||
|  | 
 | ||||||
|  |   @ValidateUUID({ each: true, optional: true }) | ||||||
|  |   tagIds?: string[]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class RandomSearchDto extends BaseSearchDto { | export class RandomSearchDto extends BaseSearchDto { | ||||||
|  | |||||||
| @ -252,6 +252,21 @@ export function hasPeople<O>(qb: SelectQueryBuilder<DB, 'assets', O>, personIds: | |||||||
|   ); |   ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function hasTags<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagIds: string[]) { | ||||||
|  |   return qb.innerJoin( | ||||||
|  |     (eb) => | ||||||
|  |       eb | ||||||
|  |         .selectFrom('tag_asset') | ||||||
|  |         .select('assetsId') | ||||||
|  |         .innerJoin('tags_closure', 'tag_asset.tagsId', 'tags_closure.id_descendant') | ||||||
|  |         .where('tags_closure.id_ancestor', '=', anyUuid(tagIds)) | ||||||
|  |         .groupBy('assetsId') | ||||||
|  |         .having((eb) => eb.fn.count('tags_closure.id_ancestor').distinct(), '>=', tagIds.length) | ||||||
|  |         .as('has_tags'), | ||||||
|  |     (join) => join.onRef('has_tags.assetsId', '=', 'assets.id'), | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function withOwner(eb: ExpressionBuilder<DB, 'assets'>) { | export function withOwner(eb: ExpressionBuilder<DB, 'assets'>) { | ||||||
|   return jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('users.id', '=', 'assets.ownerId')).as('owner'); |   return jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('users.id', '=', 'assets.ownerId')).as('owner'); | ||||||
| } | } | ||||||
| @ -326,6 +341,7 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild | |||||||
|     .withPlugin(joinDeduplicationPlugin) |     .withPlugin(joinDeduplicationPlugin) | ||||||
|     .selectFrom('assets') |     .selectFrom('assets') | ||||||
|     .selectAll('assets') |     .selectAll('assets') | ||||||
|  |     .$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!)) | ||||||
|     .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!)) |     .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!)) | ||||||
|     .$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!)) |     .$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!)) | ||||||
|     .$if(!!options.createdAfter, (qb) => qb.where('assets.createdAt', '>=', options.createdAfter!)) |     .$if(!!options.createdAfter, (qb) => qb.where('assets.createdAt', '>=', options.createdAfter!)) | ||||||
|  | |||||||
| @ -112,6 +112,10 @@ export interface SearchPeopleOptions { | |||||||
|   personIds?: string[]; |   personIds?: string[]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface SearchTagOptions { | ||||||
|  |   tagIds?: string[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export interface SearchOrderOptions { | export interface SearchOrderOptions { | ||||||
|   orderDirection?: 'asc' | 'desc'; |   orderDirection?: 'asc' | 'desc'; | ||||||
| } | } | ||||||
| @ -128,7 +132,8 @@ type BaseAssetSearchOptions = SearchDateOptions & | |||||||
|   SearchPathOptions & |   SearchPathOptions & | ||||||
|   SearchStatusOptions & |   SearchStatusOptions & | ||||||
|   SearchUserIdOptions & |   SearchUserIdOptions & | ||||||
|   SearchPeopleOptions; |   SearchPeopleOptions & | ||||||
|  |   SearchTagOptions; | ||||||
| 
 | 
 | ||||||
| export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions; | export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions; | ||||||
| 
 | 
 | ||||||
| @ -142,7 +147,8 @@ export type SmartSearchOptions = SearchDateOptions & | |||||||
|   SearchOneToOneRelationOptions & |   SearchOneToOneRelationOptions & | ||||||
|   SearchStatusOptions & |   SearchStatusOptions & | ||||||
|   SearchUserIdOptions & |   SearchUserIdOptions & | ||||||
|   SearchPeopleOptions; |   SearchPeopleOptions & | ||||||
|  |   SearchTagOptions; | ||||||
| 
 | 
 | ||||||
| export interface FaceEmbeddingSearch extends SearchEmbeddingOptions { | export interface FaceEmbeddingSearch extends SearchEmbeddingOptions { | ||||||
|   hasPerson?: boolean; |   hasPerson?: boolean; | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ | |||||||
|     query: string; |     query: string; | ||||||
|     queryType: 'smart' | 'metadata'; |     queryType: 'smart' | 'metadata'; | ||||||
|     personIds: SvelteSet<string>; |     personIds: SvelteSet<string>; | ||||||
|  |     tagIds: SvelteSet<string>; | ||||||
|     location: SearchLocationFilter; |     location: SearchLocationFilter; | ||||||
|     camera: SearchCameraFilter; |     camera: SearchCameraFilter; | ||||||
|     date: SearchDateFilter; |     date: SearchDateFilter; | ||||||
| @ -20,6 +21,7 @@ | |||||||
|   import { Button } from '@immich/ui'; |   import { Button } from '@immich/ui'; | ||||||
|   import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk'; |   import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk'; | ||||||
|   import SearchPeopleSection from './search-people-section.svelte'; |   import SearchPeopleSection from './search-people-section.svelte'; | ||||||
|  |   import SearchTagsSection from './search-tags-section.svelte'; | ||||||
|   import SearchLocationSection from './search-location-section.svelte'; |   import SearchLocationSection from './search-location-section.svelte'; | ||||||
|   import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte'; |   import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte'; | ||||||
|   import SearchDateSection from './search-date-section.svelte'; |   import SearchDateSection from './search-date-section.svelte'; | ||||||
| @ -54,6 +56,7 @@ | |||||||
|     query: 'query' in searchQuery ? searchQuery.query : searchQuery.originalFileName || '', |     query: 'query' in searchQuery ? searchQuery.query : searchQuery.originalFileName || '', | ||||||
|     queryType: 'query' in searchQuery ? 'smart' : 'metadata', |     queryType: 'query' in searchQuery ? 'smart' : 'metadata', | ||||||
|     personIds: new SvelteSet('personIds' in searchQuery ? searchQuery.personIds : []), |     personIds: new SvelteSet('personIds' in searchQuery ? searchQuery.personIds : []), | ||||||
|  |     tagIds: new SvelteSet('tagIds' in searchQuery ? searchQuery.tagIds : []), | ||||||
|     location: { |     location: { | ||||||
|       country: withNullAsUndefined(searchQuery.country), |       country: withNullAsUndefined(searchQuery.country), | ||||||
|       state: withNullAsUndefined(searchQuery.state), |       state: withNullAsUndefined(searchQuery.state), | ||||||
| @ -85,6 +88,7 @@ | |||||||
|       query: '', |       query: '', | ||||||
|       queryType: 'smart', |       queryType: 'smart', | ||||||
|       personIds: new SvelteSet(), |       personIds: new SvelteSet(), | ||||||
|  |       tagIds: new SvelteSet(), | ||||||
|       location: {}, |       location: {}, | ||||||
|       camera: {}, |       camera: {}, | ||||||
|       date: {}, |       date: {}, | ||||||
| @ -117,6 +121,7 @@ | |||||||
|       isFavorite: filter.display.isFavorite || undefined, |       isFavorite: filter.display.isFavorite || undefined, | ||||||
|       isNotInAlbum: filter.display.isNotInAlbum || undefined, |       isNotInAlbum: filter.display.isNotInAlbum || undefined, | ||||||
|       personIds: filter.personIds.size > 0 ? [...filter.personIds] : undefined, |       personIds: filter.personIds.size > 0 ? [...filter.personIds] : undefined, | ||||||
|  |       tagIds: filter.tagIds.size > 0 ? [...filter.tagIds] : undefined, | ||||||
|       type, |       type, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
| @ -143,6 +148,9 @@ | |||||||
|       <!-- TEXT --> |       <!-- TEXT --> | ||||||
|       <SearchTextSection bind:query={filter.query} bind:queryType={filter.queryType} /> |       <SearchTextSection bind:query={filter.query} bind:queryType={filter.queryType} /> | ||||||
| 
 | 
 | ||||||
|  |       <!-- TAGS --> | ||||||
|  |       <SearchTagsSection bind:selectedTags={filter.tagIds} /> | ||||||
|  | 
 | ||||||
|       <!-- LOCATION --> |       <!-- LOCATION --> | ||||||
|       <SearchLocationSection bind:filters={filter.location} /> |       <SearchLocationSection bind:filters={filter.location} /> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -0,0 +1,80 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte'; | ||||||
|  |   import { getAllTags, type TagResponseDto } from '@immich/sdk'; | ||||||
|  |   import { t } from 'svelte-i18n'; | ||||||
|  |   import { onMount } from 'svelte'; | ||||||
|  |   import { SvelteSet } from 'svelte/reactivity'; | ||||||
|  |   import Icon from '$lib/components/elements/icon.svelte'; | ||||||
|  |   import { mdiClose } from '@mdi/js'; | ||||||
|  |   import { preferences } from '$lib/stores/user.store'; | ||||||
|  | 
 | ||||||
|  |   interface Props { | ||||||
|  |     selectedTags: SvelteSet<string>; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let { selectedTags = $bindable() }: Props = $props(); | ||||||
|  | 
 | ||||||
|  |   let allTags: TagResponseDto[] = $state([]); | ||||||
|  |   let tagMap = $derived(Object.fromEntries(allTags.map((tag) => [tag.id, tag]))); | ||||||
|  |   let selectedOption = $state(undefined); | ||||||
|  | 
 | ||||||
|  |   onMount(async () => { | ||||||
|  |     allTags = await getAllTags(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   const handleSelect = (option?: ComboBoxOption) => { | ||||||
|  |     if (!option || !option.id) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     selectedTags.add(option.value); | ||||||
|  |     selectedOption = undefined; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleRemove = (tag: string) => { | ||||||
|  |     selectedTags.delete(tag); | ||||||
|  |   }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if $preferences?.tags?.enabled} | ||||||
|  |   <div id="location-selection"> | ||||||
|  |     <form autocomplete="off" id="create-tag-form"> | ||||||
|  |       <div class="my-4 flex flex-col gap-2"> | ||||||
|  |         <Combobox | ||||||
|  |           onSelect={handleSelect} | ||||||
|  |           label={$t('tags').toUpperCase()} | ||||||
|  |           defaultFirstOption | ||||||
|  |           options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))} | ||||||
|  |           bind:selectedOption | ||||||
|  |           placeholder={$t('search_tags')} | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |     </form> | ||||||
|  | 
 | ||||||
|  |     <section class="flex flex-wrap pt-2 gap-1"> | ||||||
|  |       {#each selectedTags as tagId (tagId)} | ||||||
|  |         {@const tag = tagMap[tagId]} | ||||||
|  |         {#if tag} | ||||||
|  |           <div class="flex group transition-all"> | ||||||
|  |             <span | ||||||
|  |               class="inline-block h-min whitespace-nowrap pl-3 pr-1 group-hover:pl-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary rounded-tl-full rounded-bl-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all" | ||||||
|  |             > | ||||||
|  |               <p class="text-sm"> | ||||||
|  |                 {tag.value} | ||||||
|  |               </p> | ||||||
|  |             </span> | ||||||
|  | 
 | ||||||
|  |             <button | ||||||
|  |               type="button" | ||||||
|  |               class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-tr-full rounded-br-full place-items-center place-content-center pr-2 pl-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all" | ||||||
|  |               title="Remove tag" | ||||||
|  |               onclick={() => handleRemove(tagId)} | ||||||
|  |             > | ||||||
|  |               <Icon path={mdiClose} /> | ||||||
|  |             </button> | ||||||
|  |           </div> | ||||||
|  |         {/if} | ||||||
|  |       {/each} | ||||||
|  |     </section> | ||||||
|  |   </div> | ||||||
|  | {/if} | ||||||
| @ -29,6 +29,7 @@ | |||||||
|     type SmartSearchDto, |     type SmartSearchDto, | ||||||
|     type MetadataSearchDto, |     type MetadataSearchDto, | ||||||
|     type AlbumResponseDto, |     type AlbumResponseDto, | ||||||
|  |     getTagById, | ||||||
|   } from '@immich/sdk'; |   } from '@immich/sdk'; | ||||||
|   import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; |   import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; | ||||||
|   import type { Viewport } from '$lib/stores/assets.store'; |   import type { Viewport } from '$lib/stores/assets.store'; | ||||||
| @ -194,6 +195,7 @@ | |||||||
|       model: $t('camera_model'), |       model: $t('camera_model'), | ||||||
|       lensModel: $t('lens_model'), |       lensModel: $t('lens_model'), | ||||||
|       personIds: $t('people'), |       personIds: $t('people'), | ||||||
|  |       tagIds: $t('tags'), | ||||||
|       originalFileName: $t('file_name'), |       originalFileName: $t('file_name'), | ||||||
|     }; |     }; | ||||||
|     return keyMap[key] || key; |     return keyMap[key] || key; | ||||||
| @ -215,6 +217,18 @@ | |||||||
|     return personNames.join(', '); |     return personNames.join(', '); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async function getTagNames(tagIds: string[]) { | ||||||
|  |     const tagNames = await Promise.all( | ||||||
|  |       tagIds.map(async (tagId) => { | ||||||
|  |         const tag = await getTagById({ id: tagId }); | ||||||
|  | 
 | ||||||
|  |         return tag.value; | ||||||
|  |       }), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     return tagNames.join(', '); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   const triggerAssetUpdate = () => (searchResultAssets = searchResultAssets); |   const triggerAssetUpdate = () => (searchResultAssets = searchResultAssets); | ||||||
| 
 | 
 | ||||||
|   const onAddToAlbum = (assetIds: string[]) => { |   const onAddToAlbum = (assetIds: string[]) => { | ||||||
| @ -299,6 +313,10 @@ | |||||||
|               {#await getPersonName(value) then personName} |               {#await getPersonName(value) then personName} | ||||||
|                 {personName} |                 {personName} | ||||||
|               {/await} |               {/await} | ||||||
|  |             {:else if key === 'tagIds' && Array.isArray(value)} | ||||||
|  |               {#await getTagNames(value) then tagNames} | ||||||
|  |                 {tagNames} | ||||||
|  |               {/await} | ||||||
|             {:else if value === null || value === ''} |             {:else if value === null || value === ''} | ||||||
|               {$t('unknown')} |               {$t('unknown')} | ||||||
|             {:else} |             {:else} | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user