mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -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