From 318ab756cbc04d4e2484d7b7f646287ad8eb7e0f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 30 Oct 2024 14:27:13 -0500 Subject: [PATCH] fix(mobile): search page (#13833) * fix(mobile): search page minor problems * fix: flashing between search * restore search size * remove print statement * linting --- .../models/search/search_result.model.dart | 37 ++++++ mobile/lib/pages/search/search.page.dart | 124 ++++++++++-------- .../search/paginated_search.provider.dart | 61 ++++----- .../search/paginated_search.provider.g.dart | Bin 1600 -> 1044 bytes mobile/lib/services/search.service.dart | 12 +- 5 files changed, 140 insertions(+), 94 deletions(-) create mode 100644 mobile/lib/models/search/search_result.model.dart diff --git a/mobile/lib/models/search/search_result.model.dart b/mobile/lib/models/search/search_result.model.dart new file mode 100644 index 0000000000..f51353ad61 --- /dev/null +++ b/mobile/lib/models/search/search_result.model.dart @@ -0,0 +1,37 @@ +import 'package:collection/collection.dart'; + +import 'package:immich_mobile/entities/asset.entity.dart'; + +class SearchResult { + final List assets; + final int? nextPage; + + SearchResult({ + required this.assets, + this.nextPage, + }); + + SearchResult copyWith({ + List? assets, + int? nextPage, + }) { + return SearchResult( + assets: assets ?? this.assets, + nextPage: nextPage ?? this.nextPage, + ); + } + + @override + String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)'; + + @override + bool operator ==(covariant SearchResult other) { + if (identical(this, other)) return true; + final listEquals = const DeepCollectionEquality().equals; + + return listEquals(other.assets, assets) && other.nextPage == nextPage; + } + + @override + int get hashCode => assets.hashCode ^ nextPage.hashCode; +} diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 60e61da4cc..83220cff15 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -58,23 +58,22 @@ class SearchPage extends HookConsumerWidget { final mediaTypeCurrentFilterWidget = useState(null); final displayOptionCurrentFilterWidget = useState(null); - final currentPage = useState(1); - final searchProvider = ref.watch(paginatedSearchProvider); - final searchResultCount = useState(0); + final isSearching = useState(false); search() async { if (prefilter == null && filter.value == previousFilter.value) return; + isSearching.value = true; ref.watch(paginatedSearchProvider.notifier).clear(); - - currentPage.value = 1; - - final searchResult = await ref - .watch(paginatedSearchProvider.notifier) - .getNextPage(filter.value, currentPage.value); - + await ref.watch(paginatedSearchProvider.notifier).search(filter.value); previousFilter.value = filter.value; - searchResultCount.value = searchResult.length; + isSearching.value = false; + } + + loadMoreSearchResult() async { + isSearching.value = true; + await ref.watch(paginatedSearchProvider.notifier).search(filter.value); + isSearching.value = false; } searchPrefilter() { @@ -97,20 +96,16 @@ class SearchPage extends HookConsumerWidget { useEffect( () { + Future.microtask( + () => ref.invalidate(paginatedSearchProvider), + ); searchPrefilter(); + return null; }, [], ); - loadMoreSearchResult() async { - currentPage.value += 1; - final searchResult = await ref - .watch(paginatedSearchProvider.notifier) - .getNextPage(filter.value, currentPage.value); - searchResultCount.value = searchResult.length; - } - showPeoplePicker() { handleOnSelect(Set value) { filter.value = filter.value.copyWith( @@ -465,41 +460,6 @@ class SearchPage extends HookConsumerWidget { search(); } - buildSearchResult() { - return switch (searchProvider) { - AsyncData() => Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: NotificationListener( - onNotification: (notification) { - final metrics = notification.metrics; - final shouldLoadMore = searchResultCount.value > 75; - if (metrics.pixels >= metrics.maxScrollExtent && - shouldLoadMore) { - loadMoreSearchResult(); - } - return true; - }, - child: MultiselectGrid( - renderListProvider: paginatedSearchRenderListProvider, - archiveEnabled: true, - deleteEnabled: true, - editEnabled: true, - favoriteEnabled: true, - stackEnabled: false, - emptyIndicator: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SearchEmptyContent(), - ), - ), - ), - ), - ), - AsyncError(:final error) => Text('Error: $error'), - _ => const Expanded(child: Center(child: CircularProgressIndicator())), - }; - } - return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( @@ -635,13 +595,67 @@ class SearchPage extends HookConsumerWidget { ), ), ), - buildSearchResult(), + SearchResultGrid( + onScrollEnd: loadMoreSearchResult, + isSearching: isSearching.value, + ), ], ), ); } } +class SearchResultGrid extends StatelessWidget { + final VoidCallback onScrollEnd; + final bool isSearching; + + const SearchResultGrid({ + super.key, + required this.onScrollEnd, + this.isSearching = false, + }); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: NotificationListener( + onNotification: (notification) { + final isBottomSheetNotification = notification.context + ?.findAncestorWidgetOfExactType< + DraggableScrollableSheet>() != + null; + + final metrics = notification.metrics; + final isVerticalScroll = metrics.axis == Axis.vertical; + + if (metrics.pixels >= metrics.maxScrollExtent && + isVerticalScroll && + !isBottomSheetNotification) { + onScrollEnd(); + } + + return true; + }, + child: MultiselectGrid( + renderListProvider: paginatedSearchRenderListProvider, + archiveEnabled: true, + deleteEnabled: true, + editEnabled: true, + favoriteEnabled: true, + stackEnabled: false, + emptyIndicator: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: !isSearching ? SearchEmptyContent() : SizedBox.shrink(), + ), + ), + ), + ), + ); + } +} + class SearchEmptyContent extends StatelessWidget { const SearchEmptyContent({super.key}); diff --git a/mobile/lib/providers/search/paginated_search.provider.dart b/mobile/lib/providers/search/paginated_search.provider.dart index abf711f0ad..270f1148e8 100644 --- a/mobile/lib/providers/search/paginated_search.provider.dart +++ b/mobile/lib/providers/search/paginated_search.provider.dart @@ -1,46 +1,39 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/search/search_result.model.dart'; import 'package:immich_mobile/providers/asset_viewer/render_list.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/services/search.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'paginated_search.provider.g.dart'; -@riverpod -class PaginatedSearch extends _$PaginatedSearch { - Future?> _search(SearchFilter filter, int page) async { - final service = ref.read(searchServiceProvider); - final result = await service.search(filter, page); +final paginatedSearchProvider = + StateNotifierProvider( + (ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)), +); - return result; - } +class PaginatedSearchNotifier extends StateNotifier { + final SearchService _searchService; - @override - Future> build() async { - return []; - } + PaginatedSearchNotifier(this._searchService) + : super(SearchResult(assets: [], nextPage: 1)); - Future> getNextPage(SearchFilter filter, int nextPage) async { - state = const AsyncValue.loading(); + search(SearchFilter filter) async { + if (state.nextPage == null) return; - final newState = await AsyncValue.guard(() async { - final assets = await _search(filter, nextPage); + final result = await _searchService.search(filter, state.nextPage!); - if (assets != null) { - return [...?state.value, ...assets]; - } - }); + if (result == null) return; - state = newState.valueOrNull == null - ? const AsyncValue.data([]) - : AsyncValue.data(newState.value!); - - return newState.valueOrNull ?? []; + state = SearchResult( + assets: [...state.assets, ...result.assets], + nextPage: result.nextPage, + ); } clear() { - state = const AsyncValue.data([]); + state = SearchResult(assets: [], nextPage: 1); } } @@ -48,15 +41,11 @@ class PaginatedSearch extends _$PaginatedSearch { AsyncValue paginatedSearchRenderList( PaginatedSearchRenderListRef ref, ) { - final assets = ref.watch(paginatedSearchProvider).value; + final result = ref.watch(paginatedSearchProvider); - if (assets != null) { - return ref.watch( - renderListProviderWithGrouping( - (assets, GroupAssetsBy.none), - ), - ); - } else { - return const AsyncValue.loading(); - } + return ref.watch( + renderListProviderWithGrouping( + (result.assets, GroupAssetsBy.none), + ), + ); } diff --git a/mobile/lib/providers/search/paginated_search.provider.g.dart b/mobile/lib/providers/search/paginated_search.provider.g.dart index 3357be7776450e29de81dac83e584af2bd697849..cdf8cdd741a8d8d73c6bfa3116f5aecff306a9d8 100644 GIT binary patch delta 57 zcmX@WGlgS=6{CiUsfB5>g|U&LfmxEFS&~_bk)?@&scD*tc~VkRs)cc?frWvQg^Ahb NBt}`L$*WnX0sy+y4~zf+ delta 422 zcmZ{fK}y3w6ozTtNbm~6EDBAbHqE4&PHIvthzN=Vx>PAMlQ-=^P0GxusEZyVt~`a{ z1-yzkaW)ki@GT$j|G)3OAATQwPFlXeLg0vo00bD76o#}Q#YlvhCY;NmAUq+XF?{jX zaeF(rT9r$*=xo?hmCQmh2d2g9l9`p?L)jGRE{#GgqnKh6MuZB9c^r>1r%@t{!Vx5g zgb@tgQ_l;60L=jqTbl~q=KH1h1Me)?)srefUsT%9*>(@w^Q3cFqlU{af-(=enLXO- zT$xQ}U}j!QaZ_0(73ker+MxE)wK6uH83T4Y9jMZRb`;;0?> search(SearchFilter filter, int page) async { + Future search(SearchFilter filter, int page) async { try { SearchResponseDto? response; AssetTypeEnum? type; @@ -103,8 +105,12 @@ class SearchService { return null; } - return _assetRepository - .getAllByRemoteId(response.assets.items.map((e) => e.id)); + return SearchResult( + assets: await _assetRepository.getAllByRemoteId( + response.assets.items.map((e) => e.id), + ), + nextPage: response.assets.nextPage?.toInt(), + ); } catch (error, stackTrace) { _log.severe("Failed to search for assets", error, stackTrace); }