mirror of
https://github.com/immich-app/immich.git
synced 2025-06-03 13:44:16 -04:00
feat: search by description (#15818)
* feat: search by description * wip: mobile * wip: mobile ui * wip: mobile search logic * feat: using f_unaccent * icon to fit with text search
This commit is contained in:
parent
a808a840c8
commit
4efacfbb91
@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"search_by_description_example": "Hiking day in Sapa",
|
||||||
|
"search_by_description": "Search by description",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"account_settings": "Account Settings",
|
"account_settings": "Account Settings",
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"search_filter_contextual": "Search by context",
|
||||||
|
"search_filter_filename": "Search by file name",
|
||||||
|
"search_filter_description": "Search by description",
|
||||||
"search_no_result": "No results found, try a different search term or combination",
|
"search_no_result": "No results found, try a different search term or combination",
|
||||||
|
"description_search": "Hiking day in Sapa",
|
||||||
"search_no_more_result": "No more results",
|
"search_no_more_result": "No more results",
|
||||||
"action_common_back": "Back",
|
"action_common_back": "Back",
|
||||||
"action_common_cancel": "Cancel",
|
"action_common_cancel": "Cancel",
|
||||||
|
@ -2,3 +2,9 @@ enum SortOrder {
|
|||||||
asc,
|
asc,
|
||||||
desc,
|
desc,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TextSearchType {
|
||||||
|
context,
|
||||||
|
filename,
|
||||||
|
description,
|
||||||
|
}
|
||||||
|
@ -235,6 +235,7 @@ class SearchDisplayFilters {
|
|||||||
class SearchFilter {
|
class SearchFilter {
|
||||||
String? context;
|
String? context;
|
||||||
String? filename;
|
String? filename;
|
||||||
|
String? description;
|
||||||
Set<Person> people;
|
Set<Person> people;
|
||||||
SearchLocationFilter location;
|
SearchLocationFilter location;
|
||||||
SearchCameraFilter camera;
|
SearchCameraFilter camera;
|
||||||
@ -247,6 +248,7 @@ class SearchFilter {
|
|||||||
SearchFilter({
|
SearchFilter({
|
||||||
this.context,
|
this.context,
|
||||||
this.filename,
|
this.filename,
|
||||||
|
this.description,
|
||||||
required this.people,
|
required this.people,
|
||||||
required this.location,
|
required this.location,
|
||||||
required this.camera,
|
required this.camera,
|
||||||
@ -258,6 +260,7 @@ class SearchFilter {
|
|||||||
bool get isEmpty {
|
bool get isEmpty {
|
||||||
return (context == null || (context != null && context!.isEmpty)) &&
|
return (context == null || (context != null && context!.isEmpty)) &&
|
||||||
(filename == null || (filename!.isEmpty)) &&
|
(filename == null || (filename!.isEmpty)) &&
|
||||||
|
(description == null || (description!.isEmpty)) &&
|
||||||
people.isEmpty &&
|
people.isEmpty &&
|
||||||
location.country == null &&
|
location.country == null &&
|
||||||
location.state == null &&
|
location.state == null &&
|
||||||
@ -275,6 +278,7 @@ class SearchFilter {
|
|||||||
SearchFilter copyWith({
|
SearchFilter copyWith({
|
||||||
String? context,
|
String? context,
|
||||||
String? filename,
|
String? filename,
|
||||||
|
String? description,
|
||||||
Set<Person>? people,
|
Set<Person>? people,
|
||||||
SearchLocationFilter? location,
|
SearchLocationFilter? location,
|
||||||
SearchCameraFilter? camera,
|
SearchCameraFilter? camera,
|
||||||
@ -285,6 +289,7 @@ class SearchFilter {
|
|||||||
return SearchFilter(
|
return SearchFilter(
|
||||||
context: context ?? this.context,
|
context: context ?? this.context,
|
||||||
filename: filename ?? this.filename,
|
filename: filename ?? this.filename,
|
||||||
|
description: description ?? this.description,
|
||||||
people: people ?? this.people,
|
people: people ?? this.people,
|
||||||
location: location ?? this.location,
|
location: location ?? this.location,
|
||||||
camera: camera ?? this.camera,
|
camera: camera ?? this.camera,
|
||||||
@ -296,7 +301,7 @@ class SearchFilter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SearchFilter(context: $context, filename: $filename, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)';
|
return 'SearchFilter(context: $context, filename: $filename, description: $description, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -305,6 +310,7 @@ class SearchFilter {
|
|||||||
|
|
||||||
return other.context == context &&
|
return other.context == context &&
|
||||||
other.filename == filename &&
|
other.filename == filename &&
|
||||||
|
other.description == description &&
|
||||||
other.people == people &&
|
other.people == people &&
|
||||||
other.location == location &&
|
other.location == location &&
|
||||||
other.camera == camera &&
|
other.camera == camera &&
|
||||||
@ -317,6 +323,7 @@ class SearchFilter {
|
|||||||
int get hashCode {
|
int get hashCode {
|
||||||
return context.hashCode ^
|
return context.hashCode ^
|
||||||
filename.hashCode ^
|
filename.hashCode ^
|
||||||
|
description.hashCode ^
|
||||||
people.hashCode ^
|
people.hashCode ^
|
||||||
location.hashCode ^
|
location.hashCode ^
|
||||||
camera.hashCode ^
|
camera.hashCode ^
|
||||||
|
@ -5,6 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
@ -31,7 +32,8 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isContextualSearch = useState(true);
|
final textSearchType = useState<TextSearchType>(TextSearchType.context);
|
||||||
|
final searchHintText = useState<String>('contextual_search'.tr());
|
||||||
final textSearchController = useTextEditingController();
|
final textSearchController = useTextEditingController();
|
||||||
final filter = useState<SearchFilter>(
|
final filter = useState<SearchFilter>(
|
||||||
SearchFilter(
|
SearchFilter(
|
||||||
@ -478,37 +480,148 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTextSubmitted(String value) {
|
handleTextSubmitted(String value) {
|
||||||
if (isContextualSearch.value) {
|
switch (textSearchType.value) {
|
||||||
|
case TextSearchType.context:
|
||||||
filter.value = filter.value.copyWith(
|
filter.value = filter.value.copyWith(
|
||||||
filename: '',
|
filename: '',
|
||||||
context: value,
|
context: value,
|
||||||
|
description: '',
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
|
break;
|
||||||
|
case TextSearchType.filename:
|
||||||
filter.value = filter.value.copyWith(
|
filter.value = filter.value.copyWith(
|
||||||
filename: value,
|
filename: value,
|
||||||
context: '',
|
context: '',
|
||||||
|
description: '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case TextSearchType.description:
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
filename: '',
|
||||||
|
context: '',
|
||||||
|
description: value,
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
search();
|
search();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IconData getSearchPrefixIcon() {
|
||||||
|
switch (textSearchType.value) {
|
||||||
|
case TextSearchType.context:
|
||||||
|
return Icons.image_search_rounded;
|
||||||
|
case TextSearchType.filename:
|
||||||
|
return Icons.abc_rounded;
|
||||||
|
case TextSearchType.description:
|
||||||
|
return Icons.text_snippet_outlined;
|
||||||
|
default:
|
||||||
|
return Icons.search_rounded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
automaticallyImplyLeading: true,
|
automaticallyImplyLeading: true,
|
||||||
actions: [
|
actions: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 14.0),
|
padding: const EdgeInsets.only(right: 16.0),
|
||||||
child: IconButton(
|
child: MenuAnchor(
|
||||||
key: const Key('contextual_search_button'),
|
style: MenuStyle(
|
||||||
icon: isContextualSearch.value
|
elevation: const WidgetStatePropertyAll(1),
|
||||||
? const Icon(Icons.abc_rounded)
|
shape: WidgetStateProperty.all(
|
||||||
: const Icon(Icons.image_search_rounded),
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const WidgetStatePropertyAll(
|
||||||
|
EdgeInsets.all(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (
|
||||||
|
BuildContext context,
|
||||||
|
MenuController controller,
|
||||||
|
Widget? child,
|
||||||
|
) {
|
||||||
|
return IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
isContextualSearch.value = !isContextualSearch.value;
|
if (controller.isOpen) {
|
||||||
textSearchController.clear();
|
controller.close();
|
||||||
|
} else {
|
||||||
|
controller.open();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
icon: const Icon(Icons.more_vert_rounded),
|
||||||
|
tooltip: 'Show text search menu',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
menuChildren: [
|
||||||
|
MenuItemButton(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.image_search_rounded),
|
||||||
|
title: Text(
|
||||||
|
'search_filter_contextual'.tr(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: textSearchType.value == TextSearchType.context
|
||||||
|
? context.colorScheme.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
selectedColor: context.colorScheme.primary,
|
||||||
|
selected: textSearchType.value == TextSearchType.context,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
textSearchType.value = TextSearchType.context;
|
||||||
|
searchHintText.value = 'contextual_search'.tr();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.abc_rounded),
|
||||||
|
title: Text(
|
||||||
|
'search_filter_filename'.tr(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: textSearchType.value == TextSearchType.filename
|
||||||
|
? context.colorScheme.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
selectedColor: context.colorScheme.primary,
|
||||||
|
selected: textSearchType.value == TextSearchType.filename,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
textSearchType.value = TextSearchType.filename;
|
||||||
|
searchHintText.value = 'filename_search'.tr();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.text_snippet_outlined),
|
||||||
|
title: Text(
|
||||||
|
'search_filter_description'.tr(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color:
|
||||||
|
textSearchType.value == TextSearchType.description
|
||||||
|
? context.colorScheme.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
selectedColor: context.colorScheme.primary,
|
||||||
|
selected:
|
||||||
|
textSearchType.value == TextSearchType.description,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
textSearchType.value = TextSearchType.description;
|
||||||
|
searchHintText.value = 'description_search'.tr();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -539,12 +652,10 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
prefixIcon: prefilter != null
|
prefixIcon: prefilter != null
|
||||||
? null
|
? null
|
||||||
: Icon(
|
: Icon(
|
||||||
Icons.search_rounded,
|
getSearchPrefixIcon(),
|
||||||
color: context.colorScheme.primary,
|
color: context.colorScheme.primary,
|
||||||
),
|
),
|
||||||
hintText: isContextualSearch.value
|
hintText: searchHintText.value,
|
||||||
? 'contextual_search'.tr()
|
|
||||||
: 'filename_search'.tr(),
|
|
||||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||||
color: context.themeData.colorScheme.onSurfaceSecondary,
|
color: context.themeData.colorScheme.onSurfaceSecondary,
|
||||||
),
|
),
|
||||||
|
@ -84,6 +84,10 @@ class SearchService {
|
|||||||
? filter.filename
|
? filter.filename
|
||||||
: null,
|
: null,
|
||||||
country: filter.location.country,
|
country: filter.location.country,
|
||||||
|
description:
|
||||||
|
filter.description != null && filter.description!.isNotEmpty
|
||||||
|
? filter.description
|
||||||
|
: null,
|
||||||
state: filter.location.state,
|
state: filter.location.state,
|
||||||
city: filter.location.city,
|
city: filter.location.city,
|
||||||
make: filter.camera.make,
|
make: filter.camera.make,
|
||||||
|
@ -168,7 +168,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
populateTestLoginInfo1() {
|
populateTestLoginInfo1() {
|
||||||
emailController.text = 'testuser@email.com';
|
emailController.text = 'testuser@email.com';
|
||||||
passwordController.text = 'password';
|
passwordController.text = 'password';
|
||||||
serverEndpointController.text = 'http://10.1.15.216:3000/api';
|
serverEndpointController.text = 'http://10.1.15.216:2283/api';
|
||||||
}
|
}
|
||||||
|
|
||||||
login() async {
|
login() async {
|
||||||
|
19
mobile/openapi/lib/model/metadata_search_dto.dart
generated
19
mobile/openapi/lib/model/metadata_search_dto.dart
generated
@ -18,6 +18,7 @@ class MetadataSearchDto {
|
|||||||
this.country,
|
this.country,
|
||||||
this.createdAfter,
|
this.createdAfter,
|
||||||
this.createdBefore,
|
this.createdBefore,
|
||||||
|
this.description,
|
||||||
this.deviceAssetId,
|
this.deviceAssetId,
|
||||||
this.deviceId,
|
this.deviceId,
|
||||||
this.encodedVideoPath,
|
this.encodedVideoPath,
|
||||||
@ -85,6 +86,14 @@ class MetadataSearchDto {
|
|||||||
///
|
///
|
||||||
DateTime? createdBefore;
|
DateTime? createdBefore;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
String? description;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// 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
|
||||||
@ -343,6 +352,7 @@ class MetadataSearchDto {
|
|||||||
other.country == country &&
|
other.country == country &&
|
||||||
other.createdAfter == createdAfter &&
|
other.createdAfter == createdAfter &&
|
||||||
other.createdBefore == createdBefore &&
|
other.createdBefore == createdBefore &&
|
||||||
|
other.description == description &&
|
||||||
other.deviceAssetId == deviceAssetId &&
|
other.deviceAssetId == deviceAssetId &&
|
||||||
other.deviceId == deviceId &&
|
other.deviceId == deviceId &&
|
||||||
other.encodedVideoPath == encodedVideoPath &&
|
other.encodedVideoPath == encodedVideoPath &&
|
||||||
@ -389,6 +399,7 @@ class MetadataSearchDto {
|
|||||||
(country == null ? 0 : country!.hashCode) +
|
(country == null ? 0 : country!.hashCode) +
|
||||||
(createdAfter == null ? 0 : createdAfter!.hashCode) +
|
(createdAfter == null ? 0 : createdAfter!.hashCode) +
|
||||||
(createdBefore == null ? 0 : createdBefore!.hashCode) +
|
(createdBefore == null ? 0 : createdBefore!.hashCode) +
|
||||||
|
(description == null ? 0 : description!.hashCode) +
|
||||||
(deviceAssetId == null ? 0 : deviceAssetId!.hashCode) +
|
(deviceAssetId == null ? 0 : deviceAssetId!.hashCode) +
|
||||||
(deviceId == null ? 0 : deviceId!.hashCode) +
|
(deviceId == null ? 0 : deviceId!.hashCode) +
|
||||||
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
|
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
|
||||||
@ -428,7 +439,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, 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]';
|
String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, 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>{};
|
||||||
@ -457,6 +468,11 @@ class MetadataSearchDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'createdBefore'] = null;
|
// json[r'createdBefore'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.description != null) {
|
||||||
|
json[r'description'] = this.description;
|
||||||
|
} else {
|
||||||
|
// json[r'description'] = null;
|
||||||
|
}
|
||||||
if (this.deviceAssetId != null) {
|
if (this.deviceAssetId != null) {
|
||||||
json[r'deviceAssetId'] = this.deviceAssetId;
|
json[r'deviceAssetId'] = this.deviceAssetId;
|
||||||
} else {
|
} else {
|
||||||
@ -643,6 +659,7 @@ class MetadataSearchDto {
|
|||||||
country: mapValueOfType<String>(json, r'country'),
|
country: mapValueOfType<String>(json, r'country'),
|
||||||
createdAfter: mapDateTime(json, r'createdAfter', r''),
|
createdAfter: mapDateTime(json, r'createdAfter', r''),
|
||||||
createdBefore: mapDateTime(json, r'createdBefore', r''),
|
createdBefore: mapDateTime(json, r'createdBefore', r''),
|
||||||
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
deviceAssetId: mapValueOfType<String>(json, r'deviceAssetId'),
|
deviceAssetId: mapValueOfType<String>(json, r'deviceAssetId'),
|
||||||
deviceId: mapValueOfType<String>(json, r'deviceId'),
|
deviceId: mapValueOfType<String>(json, r'deviceId'),
|
||||||
encodedVideoPath: mapValueOfType<String>(json, r'encodedVideoPath'),
|
encodedVideoPath: mapValueOfType<String>(json, r'encodedVideoPath'),
|
||||||
|
@ -9949,6 +9949,9 @@
|
|||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"deviceAssetId": {
|
"deviceAssetId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -769,6 +769,7 @@ export type MetadataSearchDto = {
|
|||||||
country?: string | null;
|
country?: string | null;
|
||||||
createdAfter?: string;
|
createdAfter?: string;
|
||||||
createdBefore?: string;
|
createdBefore?: string;
|
||||||
|
description?: string;
|
||||||
deviceAssetId?: string;
|
deviceAssetId?: string;
|
||||||
deviceId?: string;
|
deviceId?: string;
|
||||||
encodedVideoPath?: string;
|
encodedVideoPath?: string;
|
||||||
|
@ -133,6 +133,11 @@ export class MetadataSearchDto extends RandomSearchDto {
|
|||||||
@Optional()
|
@Optional()
|
||||||
deviceAssetId?: string;
|
deviceAssetId?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Optional()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Optional()
|
@Optional()
|
||||||
|
@ -396,6 +396,11 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
|
|||||||
sql`'%' || f_unaccent(${options.originalFileName}) || '%'`,
|
sql`'%' || f_unaccent(${options.originalFileName}) || '%'`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.$if(!!options.description, (qb) =>
|
||||||
|
qb
|
||||||
|
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
.where(sql`f_unaccent(exif.description)`, 'ilike', sql`'%' || f_unaccent(${options.description}) || '%'`),
|
||||||
|
)
|
||||||
.$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!))
|
.$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!))
|
||||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
||||||
.$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!))
|
.$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!))
|
||||||
|
@ -101,6 +101,7 @@ export interface SearchExifOptions {
|
|||||||
make?: string | null;
|
make?: string | null;
|
||||||
model?: string | null;
|
model?: string | null;
|
||||||
state?: string | null;
|
state?: string | null;
|
||||||
|
description?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchEmbeddingOptions {
|
export interface SearchEmbeddingOptions {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
export type SearchFilter = {
|
export type SearchFilter = {
|
||||||
query: string;
|
query: string;
|
||||||
queryType: 'smart' | 'metadata';
|
queryType: 'smart' | 'metadata' | 'description';
|
||||||
personIds: SvelteSet<string>;
|
personIds: SvelteSet<string>;
|
||||||
tagIds: SvelteSet<string>;
|
tagIds: SvelteSet<string>;
|
||||||
location: SearchLocationFilter;
|
location: SearchLocationFilter;
|
||||||
@ -110,6 +110,7 @@
|
|||||||
let payload: SmartSearchDto | MetadataSearchDto = {
|
let payload: SmartSearchDto | MetadataSearchDto = {
|
||||||
query: filter.queryType === 'smart' ? query : undefined,
|
query: filter.queryType === 'smart' ? query : undefined,
|
||||||
originalFileName: filter.queryType === 'metadata' ? query : undefined,
|
originalFileName: filter.queryType === 'metadata' ? query : undefined,
|
||||||
|
description: filter.queryType === 'description' ? query : undefined,
|
||||||
country: filter.location.country,
|
country: filter.location.country,
|
||||||
state: filter.location.state,
|
state: filter.location.state,
|
||||||
city: filter.location.city,
|
city: filter.location.city,
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
query: string | undefined;
|
query: string | undefined;
|
||||||
queryType?: 'smart' | 'metadata';
|
queryType?: 'smart' | 'metadata' | 'description';
|
||||||
}
|
}
|
||||||
|
|
||||||
let { query = $bindable(), queryType = $bindable('smart') }: Props = $props();
|
let { query = $bindable(), queryType = $bindable('smart') }: Props = $props();
|
||||||
@ -21,6 +21,13 @@
|
|||||||
bind:group={queryType}
|
bind:group={queryType}
|
||||||
value="metadata"
|
value="metadata"
|
||||||
/>
|
/>
|
||||||
|
<RadioButton
|
||||||
|
name="query-type"
|
||||||
|
id="description-radio"
|
||||||
|
label={$t('description')}
|
||||||
|
bind:group={queryType}
|
||||||
|
value="description"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@ -34,7 +41,7 @@
|
|||||||
placeholder={$t('sunrise_on_the_beach')}
|
placeholder={$t('sunrise_on_the_beach')}
|
||||||
bind:value={query}
|
bind:value={query}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else if queryType === 'metadata'}
|
||||||
<label for="file-name-input" class="immich-form-label">{$t('search_by_filename')}</label>
|
<label for="file-name-input" class="immich-form-label">{$t('search_by_filename')}</label>
|
||||||
<input
|
<input
|
||||||
class="immich-form-input hover:cursor-text w-full !mt-1"
|
class="immich-form-input hover:cursor-text w-full !mt-1"
|
||||||
@ -45,4 +52,15 @@
|
|||||||
bind:value={query}
|
bind:value={query}
|
||||||
aria-labelledby="file-name-label"
|
aria-labelledby="file-name-label"
|
||||||
/>
|
/>
|
||||||
|
{:else if queryType === 'description'}
|
||||||
|
<label for="description-input" class="immich-form-label">{$t('search_by_description')}</label>
|
||||||
|
<input
|
||||||
|
class="immich-form-input hover:cursor-text w-full !mt-1"
|
||||||
|
type="text"
|
||||||
|
id="description-input"
|
||||||
|
name="description"
|
||||||
|
placeholder={$t('search_by_description_example')}
|
||||||
|
bind:value={query}
|
||||||
|
aria-labelledby="description-label"
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -197,6 +197,7 @@
|
|||||||
personIds: $t('people'),
|
personIds: $t('people'),
|
||||||
tagIds: $t('tags'),
|
tagIds: $t('tags'),
|
||||||
originalFileName: $t('file_name'),
|
originalFileName: $t('file_name'),
|
||||||
|
description: $t('description'),
|
||||||
};
|
};
|
||||||
return keyMap[key] || key;
|
return keyMap[key] || key;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user