diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index c14aa6d748..61c4621346 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -1,4 +1,6 @@ { + "search_no_result": "No results found, try a different search term or combination", + "search_no_more_result": "No more results", "action_common_back": "Back", "action_common_cancel": "Cancel", "action_common_clear": "Clear", diff --git a/mobile/lib/interfaces/person_api.interface.dart b/mobile/lib/interfaces/person_api.interface.dart index b2fa28df8c..9d127ad765 100644 --- a/mobile/lib/interfaces/person_api.interface.dart +++ b/mobile/lib/interfaces/person_api.interface.dart @@ -1,3 +1,6 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + abstract interface class IPersonApiRepository { Future> getAll(); Future update(String id, {String? name}); @@ -6,10 +9,10 @@ abstract interface class IPersonApiRepository { class Person { Person({ required this.id, + this.birthDate, required this.isHidden, required this.name, required this.thumbnailPath, - this.birthDate, this.updatedAt, }); @@ -19,4 +22,80 @@ class Person { final String name; final String thumbnailPath; final DateTime? updatedAt; + + @override + String toString() { + return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)'; + } + + Person copyWith({ + String? id, + DateTime? birthDate, + bool? isHidden, + String? name, + String? thumbnailPath, + DateTime? updatedAt, + }) { + return Person( + id: id ?? this.id, + birthDate: birthDate ?? this.birthDate, + isHidden: isHidden ?? this.isHidden, + name: name ?? this.name, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + Map toMap() { + return { + 'id': id, + 'birthDate': birthDate?.millisecondsSinceEpoch, + 'isHidden': isHidden, + 'name': name, + 'thumbnailPath': thumbnailPath, + 'updatedAt': updatedAt?.millisecondsSinceEpoch, + }; + } + + factory Person.fromMap(Map map) { + return Person( + id: map['id'] as String, + birthDate: map['birthDate'] != null + ? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int) + : null, + isHidden: map['isHidden'] as bool, + name: map['name'] as String, + thumbnailPath: map['thumbnailPath'] as String, + updatedAt: map['updatedAt'] != null + ? DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int) + : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory Person.fromJson(String source) => + Person.fromMap(json.decode(source) as Map); + + @override + bool operator ==(covariant Person other) { + if (identical(this, other)) return true; + + return other.id == id && + other.birthDate == birthDate && + other.isHidden == isHidden && + other.name == name && + other.thumbnailPath == thumbnailPath && + other.updatedAt == updatedAt; + } + + @override + int get hashCode { + return id.hashCode ^ + birthDate.hashCode ^ + isHidden.hashCode ^ + name.hashCode ^ + thumbnailPath.hashCode ^ + updatedAt.hashCode; + } } diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 297a819b6a..0df64b6924 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -255,6 +255,23 @@ class SearchFilter { required this.mediaType, }); + bool get isEmpty { + return (context == null || (context != null && context!.isEmpty)) && + (filename == null || (filename!.isEmpty)) && + people.isEmpty && + location.country == null && + location.state == null && + location.city == null && + camera.make == null && + camera.model == null && + date.takenBefore == null && + date.takenAfter == null && + display.isNotInAlbum == false && + display.isArchive == false && + display.isFavorite == false && + mediaType == AssetType.other; + } + SearchFilter copyWith({ String? context, String? filename, diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 88cc56a145..385da9dbcb 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -49,7 +49,7 @@ class SearchPage extends HookConsumerWidget { ), ); - final previousFilter = useState(filter.value); + final previousFilter = useState(null); final peopleCurrentFilterWidget = useState(null); final dateRangeCurrentFilterWidget = useState(null); @@ -60,19 +60,55 @@ class SearchPage extends HookConsumerWidget { final isSearching = useState(false); + SnackBar searchInfoSnackBar(String message) { + return SnackBar( + content: Text( + message, + style: context.textTheme.labelLarge, + ), + showCloseIcon: true, + behavior: SnackBarBehavior.fixed, + closeIconColor: context.colorScheme.onSurface, + ); + } + search() async { - if (prefilter == null && filter.value == previousFilter.value) return; + if (filter.value.isEmpty) { + return; + } + + if (prefilter == null && filter.value == previousFilter.value) { + return; + } isSearching.value = true; ref.watch(paginatedSearchProvider.notifier).clear(); - await ref.watch(paginatedSearchProvider.notifier).search(filter.value); + final hasResult = await ref + .watch(paginatedSearchProvider.notifier) + .search(filter.value); + + if (!hasResult) { + context.showSnackBar( + searchInfoSnackBar('search_no_result'.tr()), + ); + } + previousFilter.value = filter.value; isSearching.value = false; } loadMoreSearchResult() async { isSearching.value = true; - await ref.watch(paginatedSearchProvider.notifier).search(filter.value); + final hasResult = await ref + .watch(paginatedSearchProvider.notifier) + .search(filter.value); + + if (!hasResult) { + context.showSnackBar( + searchInfoSnackBar('search_no_more_result'.tr()), + ); + } + isSearching.value = false; } @@ -596,10 +632,15 @@ class SearchPage extends HookConsumerWidget { ), ), ), - SearchResultGrid( - onScrollEnd: loadMoreSearchResult, - isSearching: isSearching.value, - ), + if (isSearching.value) + const Expanded( + child: Center(child: CircularProgressIndicator.adaptive()), + ) + else + SearchResultGrid( + onScrollEnd: loadMoreSearchResult, + isSearching: isSearching.value, + ), ], ), ); diff --git a/mobile/lib/providers/search/paginated_search.provider.dart b/mobile/lib/providers/search/paginated_search.provider.dart index 270f1148e8..60264947b2 100644 --- a/mobile/lib/providers/search/paginated_search.provider.dart +++ b/mobile/lib/providers/search/paginated_search.provider.dart @@ -19,17 +19,23 @@ class PaginatedSearchNotifier extends StateNotifier { PaginatedSearchNotifier(this._searchService) : super(SearchResult(assets: [], nextPage: 1)); - search(SearchFilter filter) async { - if (state.nextPage == null) return; + Future search(SearchFilter filter) async { + if (state.nextPage == null) { + return false; + } final result = await _searchService.search(filter, state.nextPage!); - if (result == null) return; + if (result == null) { + return false; + } state = SearchResult( assets: [...state.assets, ...result.assets], nextPage: result.nextPage, ); + + return true; } clear() { diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index ba46848cdd..fe8f7393c2 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -101,7 +101,7 @@ class SearchService { ); } - if (response == null) { + if (response == null || response.assets.items.isEmpty) { return null; } diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index 9cc74bf939..04f9538875 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -20,7 +20,7 @@ class PeoplePicker extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final formFocus = useFocusNode(); - final imageSize = 75.0; + final imageSize = 60.0; final searchQuery = useState(''); final people = ref.watch(getAllPeopleProvider); final headers = ApiService.getRequestHeaders();