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:
@@ -2,3 +2,9 @@ enum SortOrder {
|
||||
asc,
|
||||
desc,
|
||||
}
|
||||
|
||||
enum TextSearchType {
|
||||
context,
|
||||
filename,
|
||||
description,
|
||||
}
|
||||
|
||||
@@ -235,6 +235,7 @@ class SearchDisplayFilters {
|
||||
class SearchFilter {
|
||||
String? context;
|
||||
String? filename;
|
||||
String? description;
|
||||
Set<Person> people;
|
||||
SearchLocationFilter location;
|
||||
SearchCameraFilter camera;
|
||||
@@ -247,6 +248,7 @@ class SearchFilter {
|
||||
SearchFilter({
|
||||
this.context,
|
||||
this.filename,
|
||||
this.description,
|
||||
required this.people,
|
||||
required this.location,
|
||||
required this.camera,
|
||||
@@ -258,6 +260,7 @@ class SearchFilter {
|
||||
bool get isEmpty {
|
||||
return (context == null || (context != null && context!.isEmpty)) &&
|
||||
(filename == null || (filename!.isEmpty)) &&
|
||||
(description == null || (description!.isEmpty)) &&
|
||||
people.isEmpty &&
|
||||
location.country == null &&
|
||||
location.state == null &&
|
||||
@@ -275,6 +278,7 @@ class SearchFilter {
|
||||
SearchFilter copyWith({
|
||||
String? context,
|
||||
String? filename,
|
||||
String? description,
|
||||
Set<Person>? people,
|
||||
SearchLocationFilter? location,
|
||||
SearchCameraFilter? camera,
|
||||
@@ -285,6 +289,7 @@ class SearchFilter {
|
||||
return SearchFilter(
|
||||
context: context ?? this.context,
|
||||
filename: filename ?? this.filename,
|
||||
description: description ?? this.description,
|
||||
people: people ?? this.people,
|
||||
location: location ?? this.location,
|
||||
camera: camera ?? this.camera,
|
||||
@@ -296,7 +301,7 @@ class SearchFilter {
|
||||
|
||||
@override
|
||||
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
|
||||
@@ -305,6 +310,7 @@ class SearchFilter {
|
||||
|
||||
return other.context == context &&
|
||||
other.filename == filename &&
|
||||
other.description == description &&
|
||||
other.people == people &&
|
||||
other.location == location &&
|
||||
other.camera == camera &&
|
||||
@@ -317,6 +323,7 @@ class SearchFilter {
|
||||
int get hashCode {
|
||||
return context.hashCode ^
|
||||
filename.hashCode ^
|
||||
description.hashCode ^
|
||||
people.hashCode ^
|
||||
location.hashCode ^
|
||||
camera.hashCode ^
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.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/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
@@ -31,7 +32,8 @@ class SearchPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
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 filter = useState<SearchFilter>(
|
||||
SearchFilter(
|
||||
@@ -478,37 +480,148 @@ class SearchPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
handleTextSubmitted(String value) {
|
||||
if (isContextualSearch.value) {
|
||||
filter.value = filter.value.copyWith(
|
||||
filename: '',
|
||||
context: value,
|
||||
);
|
||||
} else {
|
||||
filter.value = filter.value.copyWith(
|
||||
filename: value,
|
||||
context: '',
|
||||
);
|
||||
switch (textSearchType.value) {
|
||||
case TextSearchType.context:
|
||||
filter.value = filter.value.copyWith(
|
||||
filename: '',
|
||||
context: value,
|
||||
description: '',
|
||||
);
|
||||
|
||||
break;
|
||||
case TextSearchType.filename:
|
||||
filter.value = filter.value.copyWith(
|
||||
filename: value,
|
||||
context: '',
|
||||
description: '',
|
||||
);
|
||||
|
||||
break;
|
||||
case TextSearchType.description:
|
||||
filter.value = filter.value.copyWith(
|
||||
filename: '',
|
||||
context: '',
|
||||
description: value,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
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(
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: true,
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 14.0),
|
||||
child: IconButton(
|
||||
key: const Key('contextual_search_button'),
|
||||
icon: isContextualSearch.value
|
||||
? const Icon(Icons.abc_rounded)
|
||||
: const Icon(Icons.image_search_rounded),
|
||||
onPressed: () {
|
||||
isContextualSearch.value = !isContextualSearch.value;
|
||||
textSearchController.clear();
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: MenuAnchor(
|
||||
style: MenuStyle(
|
||||
elevation: const WidgetStatePropertyAll(1),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(
|
||||
EdgeInsets.all(4),
|
||||
),
|
||||
),
|
||||
builder: (
|
||||
BuildContext context,
|
||||
MenuController controller,
|
||||
Widget? child,
|
||||
) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
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
|
||||
? null
|
||||
: Icon(
|
||||
Icons.search_rounded,
|
||||
getSearchPrefixIcon(),
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
hintText: isContextualSearch.value
|
||||
? 'contextual_search'.tr()
|
||||
: 'filename_search'.tr(),
|
||||
hintText: searchHintText.value,
|
||||
hintStyle: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.themeData.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
|
||||
@@ -84,6 +84,10 @@ class SearchService {
|
||||
? filter.filename
|
||||
: null,
|
||||
country: filter.location.country,
|
||||
description:
|
||||
filter.description != null && filter.description!.isNotEmpty
|
||||
? filter.description
|
||||
: null,
|
||||
state: filter.location.state,
|
||||
city: filter.location.city,
|
||||
make: filter.camera.make,
|
||||
|
||||
@@ -168,7 +168,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
populateTestLoginInfo1() {
|
||||
emailController.text = 'testuser@email.com';
|
||||
passwordController.text = 'password';
|
||||
serverEndpointController.text = 'http://10.1.15.216:3000/api';
|
||||
serverEndpointController.text = 'http://10.1.15.216:2283/api';
|
||||
}
|
||||
|
||||
login() async {
|
||||
|
||||
Reference in New Issue
Block a user