From 56cde0438c2698edde6b3f67515c8ffa2e69d389 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Fri, 5 Jan 2024 22:23:58 +0100 Subject: [PATCH] feat(mobile): multiselect for search & person page (#6016) * feat(mobile): multiselect for search & person page * merge main --------- Co-authored-by: Alex Tran --- mobile/ios/Podfile.lock | 2 +- .../providers/render_list.provider.dart | 9 ++++ .../search_result_page_state.model.dart | 8 ++- .../providers/all_video_assets.provider.dart | 12 +++-- .../search_result_page.provider.dart | 16 ++++-- .../modules/search/ui/search_result_grid.dart | 35 ------------- .../modules/search/views/all_videos_page.dart | 11 +--- .../search/views/person_result_page.dart | 51 +++++++++---------- .../search/views/search_result_page.dart | 39 ++++++-------- 9 files changed, 78 insertions(+), 105 deletions(-) delete mode 100644 mobile/lib/modules/search/ui/search_result_grid.dart diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index c6c23d942a..75168ce1c9 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -169,4 +169,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382 -COCOAPODS: 1.12.1 +COCOAPODS: 1.11.3 diff --git a/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart b/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart index c2e8782bbd..ec568425b8 100644 --- a/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart +++ b/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart @@ -16,6 +16,15 @@ final renderListProvider = ); }); +final renderListProviderWithGrouping = + FutureProvider.family, GroupAssetsBy?)>( + (ref, args) { + final settings = ref.watch(appSettingsServiceProvider); + final grouping = args.$2 ?? + GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; + return RenderList.fromAssets(args.$1, grouping); +}); + final renderListQueryProvider = StreamProvider.family?>( (ref, query) => diff --git a/mobile/lib/modules/search/models/search_result_page_state.model.dart b/mobile/lib/modules/search/models/search_result_page_state.model.dart index 6db10616f7..51d557b8c3 100644 --- a/mobile/lib/modules/search/models/search_result_page_state.model.dart +++ b/mobile/lib/modules/search/models/search_result_page_state.model.dart @@ -5,12 +5,14 @@ class SearchResultPageState { final bool isLoading; final bool isSuccess; final bool isError; + final bool isClip; final List searchResult; SearchResultPageState({ required this.isLoading, required this.isSuccess, required this.isError, + required this.isClip, required this.searchResult, }); @@ -18,19 +20,21 @@ class SearchResultPageState { bool? isLoading, bool? isSuccess, bool? isError, + bool? isClip, List? searchResult, }) { return SearchResultPageState( isLoading: isLoading ?? this.isLoading, isSuccess: isSuccess ?? this.isSuccess, isError: isError ?? this.isError, + isClip: isClip ?? this.isClip, searchResult: searchResult ?? this.searchResult, ); } @override String toString() { - return 'SearchresultPageState(isLoading: $isLoading, isSuccess: $isSuccess, isError: $isError, searchResult: $searchResult)'; + return 'SearchresultPageState(isLoading: $isLoading, isSuccess: $isSuccess, isError: $isError, isClip: $isClip, searchResult: $searchResult)'; } @override @@ -42,6 +46,7 @@ class SearchResultPageState { other.isLoading == isLoading && other.isSuccess == isSuccess && other.isError == isError && + other.isClip == isClip && listEquals(other.searchResult, searchResult); } @@ -50,6 +55,7 @@ class SearchResultPageState { return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ + isClip.hashCode ^ searchResult.hashCode; } } diff --git a/mobile/lib/modules/search/providers/all_video_assets.provider.dart b/mobile/lib/modules/search/providers/all_video_assets.provider.dart index e2c7edec97..3a3c9e6fa1 100644 --- a/mobile/lib/modules/search/providers/all_video_assets.provider.dart +++ b/mobile/lib/modules/search/providers/all_video_assets.provider.dart @@ -1,13 +1,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; -import 'package:isar/isar.dart'; +import 'package:immich_mobile/utils/renderlist_generator.dart'; -final allVideoAssetsProvider = FutureProvider>((ref) async { - return ref +final allVideoAssetsProvider = StreamProvider((ref) { + final query = ref .watch(dbProvider) .assets .filter() + .isArchivedEqualTo(false) + .isTrashedEqualTo(false) .typeEqualTo(AssetType.video) - .findAll(); + .sortByFileCreatedAtDesc(); + return renderListGenerator(query, ref); }); diff --git a/mobile/lib/modules/search/providers/search_result_page.provider.dart b/mobile/lib/modules/search/providers/search_result_page.provider.dart index 02d3ccd0f4..a481f291ce 100644 --- a/mobile/lib/modules/search/providers/search_result_page.provider.dart +++ b/mobile/lib/modules/search/providers/search_result_page.provider.dart @@ -1,5 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart'; import 'package:immich_mobile/modules/search/services/search.service.dart'; @@ -13,12 +14,13 @@ class SearchResultPageNotifier extends StateNotifier { isError: false, isLoading: true, isSuccess: false, + isClip: false, ), ); final SearchService _searchService; - void search(String searchTerm, {bool clipEnable = true}) async { + Future search(String searchTerm, {bool clipEnable = true}) async { state = state.copyWith( searchResult: [], isError: false, @@ -37,6 +39,7 @@ class SearchResultPageNotifier extends StateNotifier { isError: false, isLoading: false, isSuccess: true, + isClip: clipEnable, ); } else { state = state.copyWith( @@ -44,6 +47,7 @@ class SearchResultPageNotifier extends StateNotifier { isError: true, isLoading: false, isSuccess: false, + isClip: clipEnable, ); } } @@ -55,7 +59,11 @@ final searchResultPageProvider = return SearchResultPageNotifier(ref.watch(searchServiceProvider)); }); -final searchRenderListProvider = FutureProvider((ref) { - final assets = ref.watch(searchResultPageProvider).searchResult; - return ref.watch(renderListProvider(assets)); +final searchRenderListProvider = Provider((ref) { + final result = ref.watch(searchResultPageProvider); + return ref.watch( + renderListProviderWithGrouping( + (result.searchResult, result.isClip ? GroupAssetsBy.none : null), + ), + ); }); diff --git a/mobile/lib/modules/search/ui/search_result_grid.dart b/mobile/lib/modules/search/ui/search_result_grid.dart deleted file mode 100644 index 2975999be4..0000000000 --- a/mobile/lib/modules/search/ui/search_result_grid.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; -import 'package:immich_mobile/shared/models/asset.dart'; - -class SearchResultGrid extends HookConsumerWidget { - const SearchResultGrid({super.key, required this.assets}); - - final List assets; - - Asset _loadAsset(int index) => assets[index]; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 1, - crossAxisSpacing: 4, - mainAxisSpacing: 4, - ), - itemCount: assets.length, - itemBuilder: (context, index) { - final asset = assets[index]; - return ThumbnailImage( - asset: asset, - index: index, - loadAsset: _loadAsset, - totalAssets: assets.length, - useGrayBoxPlaceholder: true, - ); - }, - ); - } -} diff --git a/mobile/lib/modules/search/views/all_videos_page.dart b/mobile/lib/modules/search/views/all_videos_page.dart index 9db3358777..6cf344f45f 100644 --- a/mobile/lib/modules/search/views/all_videos_page.dart +++ b/mobile/lib/modules/search/views/all_videos_page.dart @@ -2,17 +2,14 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/all_video_assets.provider.dart'; +import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart'; class AllVideosPage extends HookConsumerWidget { const AllVideosPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final videos = ref.watch(allVideoAssetsProvider); - return Scaffold( appBar: AppBar( title: const Text('all_videos_page_title').tr(), @@ -21,11 +18,7 @@ class AllVideosPage extends HookConsumerWidget { icon: const Icon(Icons.arrow_back_ios_rounded), ), ), - body: videos.widgetWhen( - onData: (assets) => ImmichAssetGrid( - assets: assets, - ), - ), + body: MultiselectGrid(renderListProvider: allVideoAssetsProvider), ); } } diff --git a/mobile/lib/modules/search/views/person_result_page.dart b/mobile/lib/modules/search/views/person_result_page.dart index a1f62ae01a..a4dfa0fd3c 100644 --- a/mobile/lib/modules/search/views/person_result_page.dart +++ b/mobile/lib/modules/search/views/person_result_page.dart @@ -1,14 +1,13 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/people.provider.dart'; import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart'; -import 'package:immich_mobile/shared/models/store.dart' as isar_store; +import 'package:immich_mobile/shared/models/store.dart'; +import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; class PersonResultPage extends HookConsumerWidget { @@ -112,32 +111,30 @@ class PersonResultPage extends HookConsumerWidget { ), ], ), - body: ref.watch(personAssetsProvider(personId)).widgetWhen( - onData: (renderList) => ImmichAssetGrid( - renderList: renderList, - topWidget: Padding( - padding: const EdgeInsets.only(left: 8.0, top: 24), - child: Row( - children: [ - CircleAvatar( - radius: 36, - backgroundImage: NetworkImage( - getFaceThumbnailUrl(personId), - headers: { - "Authorization": - "Bearer ${isar_store.Store.get(isar_store.StoreKey.accessToken)}", - }, - ), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: buildTitleBlock(), - ), - ], + body: MultiselectGrid( + renderListProvider: personAssetsProvider(personId), + topWidget: Padding( + padding: const EdgeInsets.only(left: 8.0, top: 24), + child: Row( + children: [ + CircleAvatar( + radius: 36, + backgroundImage: NetworkImage( + getFaceThumbnailUrl(personId), + headers: { + "Authorization": + "Bearer ${Store.get(StoreKey.accessToken)}", + }, ), ), - ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: buildTitleBlock(), + ), + ], ), + ), + ), ); } } diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart index 585b55d713..29823d53fb 100644 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ b/mobile/lib/modules/search/views/search_result_page.dart @@ -4,11 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart'; -import 'package:immich_mobile/modules/search/ui/search_result_grid.dart'; import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; +import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; class SearchType { @@ -39,7 +38,6 @@ class SearchResultPage extends HookConsumerWidget { final searchTermController = useTextEditingController(text: ""); final isNewSearch = useState(false); final currentSearchTerm = useState(searchTerm); - final isDisplayDateGroup = useState(true); FocusNode? searchFocusNode; @@ -48,9 +46,6 @@ class SearchResultPage extends HookConsumerWidget { searchFocusNode = FocusNode(); var searchType = _getSearchType(searchTerm); - searchType.isClip - ? isDisplayDateGroup.value = false - : isDisplayDateGroup.value = true; Future.delayed( Duration.zero, @@ -63,18 +58,13 @@ class SearchResultPage extends HookConsumerWidget { [], ); - onSearchSubmitted(String newSearchTerm) { + Future onSearchSubmitted(String newSearchTerm) { debugPrint("Re-Search with $newSearchTerm"); searchFocusNode?.unfocus(); isNewSearch.value = false; currentSearchTerm.value = newSearchTerm; - var searchType = _getSearchType(newSearchTerm); - searchType.isClip - ? isDisplayDateGroup.value = false - : isDisplayDateGroup.value = true; - - ref + return ref .watch(searchResultPageProvider.notifier) .search(searchType.searchTerm, clipEnable: searchType.isClip); } @@ -148,9 +138,10 @@ class SearchResultPage extends HookConsumerWidget { ); } + Future refresh() async => onSearchSubmitted(currentSearchTerm.value); + buildSearchResult() { - var searchResultPageState = ref.watch(searchResultPageProvider); - var allSearchAssets = ref.watch(searchResultPageProvider).searchResult; + final searchResultPageState = ref.watch(searchResultPageProvider); if (searchResultPageState.isError) { return Padding( @@ -164,15 +155,15 @@ class SearchResultPage extends HookConsumerWidget { } if (searchResultPageState.isSuccess) { - if (isDisplayDateGroup.value) { - return ImmichAssetGrid( - assets: allSearchAssets, - ); - } else { - return SearchResultGrid( - assets: allSearchAssets, - ); - } + return MultiselectGrid( + renderListProvider: searchRenderListProvider, + archiveEnabled: true, + deleteEnabled: true, + editEnabled: true, + favoriteEnabled: true, + stackEnabled: false, + onRefresh: refresh, + ); } return const SizedBox();