1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(mobile): multiselect for search & person page (#6016)

* feat(mobile): multiselect for search & person page

* merge main

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Fynn Petersen-Frey 2024-01-05 22:23:58 +01:00 committed by GitHub
parent ad09896f58
commit 56cde0438c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 78 additions and 105 deletions

View File

@ -169,4 +169,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382 PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
COCOAPODS: 1.12.1 COCOAPODS: 1.11.3

View File

@ -16,6 +16,15 @@ final renderListProvider =
); );
}); });
final renderListProviderWithGrouping =
FutureProvider.family<RenderList, (List<Asset>, 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<RenderList, final renderListQueryProvider = StreamProvider.family<RenderList,
QueryBuilder<Asset, Asset, QAfterSortBy>?>( QueryBuilder<Asset, Asset, QAfterSortBy>?>(
(ref, query) => (ref, query) =>

View File

@ -5,12 +5,14 @@ class SearchResultPageState {
final bool isLoading; final bool isLoading;
final bool isSuccess; final bool isSuccess;
final bool isError; final bool isError;
final bool isClip;
final List<Asset> searchResult; final List<Asset> searchResult;
SearchResultPageState({ SearchResultPageState({
required this.isLoading, required this.isLoading,
required this.isSuccess, required this.isSuccess,
required this.isError, required this.isError,
required this.isClip,
required this.searchResult, required this.searchResult,
}); });
@ -18,19 +20,21 @@ class SearchResultPageState {
bool? isLoading, bool? isLoading,
bool? isSuccess, bool? isSuccess,
bool? isError, bool? isError,
bool? isClip,
List<Asset>? searchResult, List<Asset>? searchResult,
}) { }) {
return SearchResultPageState( return SearchResultPageState(
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
isSuccess: isSuccess ?? this.isSuccess, isSuccess: isSuccess ?? this.isSuccess,
isError: isError ?? this.isError, isError: isError ?? this.isError,
isClip: isClip ?? this.isClip,
searchResult: searchResult ?? this.searchResult, searchResult: searchResult ?? this.searchResult,
); );
} }
@override @override
String toString() { 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 @override
@ -42,6 +46,7 @@ class SearchResultPageState {
other.isLoading == isLoading && other.isLoading == isLoading &&
other.isSuccess == isSuccess && other.isSuccess == isSuccess &&
other.isError == isError && other.isError == isError &&
other.isClip == isClip &&
listEquals(other.searchResult, searchResult); listEquals(other.searchResult, searchResult);
} }
@ -50,6 +55,7 @@ class SearchResultPageState {
return isLoading.hashCode ^ return isLoading.hashCode ^
isSuccess.hashCode ^ isSuccess.hashCode ^
isError.hashCode ^ isError.hashCode ^
isClip.hashCode ^
searchResult.hashCode; searchResult.hashCode;
} }
} }

View File

@ -1,13 +1,17 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; 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/models/asset.dart';
import 'package:immich_mobile/shared/providers/db.provider.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<List<Asset>>((ref) async { final allVideoAssetsProvider = StreamProvider<RenderList>((ref) {
return ref final query = ref
.watch(dbProvider) .watch(dbProvider)
.assets .assets
.filter() .filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.typeEqualTo(AssetType.video) .typeEqualTo(AssetType.video)
.findAll(); .sortByFileCreatedAtDesc();
return renderListGenerator(query, ref);
}); });

View File

@ -1,5 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.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/models/search_result_page_state.model.dart';
import 'package:immich_mobile/modules/search/services/search.service.dart'; import 'package:immich_mobile/modules/search/services/search.service.dart';
@ -13,12 +14,13 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
isError: false, isError: false,
isLoading: true, isLoading: true,
isSuccess: false, isSuccess: false,
isClip: false,
), ),
); );
final SearchService _searchService; final SearchService _searchService;
void search(String searchTerm, {bool clipEnable = true}) async { Future<void> search(String searchTerm, {bool clipEnable = true}) async {
state = state.copyWith( state = state.copyWith(
searchResult: [], searchResult: [],
isError: false, isError: false,
@ -37,6 +39,7 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
isError: false, isError: false,
isLoading: false, isLoading: false,
isSuccess: true, isSuccess: true,
isClip: clipEnable,
); );
} else { } else {
state = state.copyWith( state = state.copyWith(
@ -44,6 +47,7 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
isError: true, isError: true,
isLoading: false, isLoading: false,
isSuccess: false, isSuccess: false,
isClip: clipEnable,
); );
} }
} }
@ -55,7 +59,11 @@ final searchResultPageProvider =
return SearchResultPageNotifier(ref.watch(searchServiceProvider)); return SearchResultPageNotifier(ref.watch(searchServiceProvider));
}); });
final searchRenderListProvider = FutureProvider((ref) { final searchRenderListProvider = Provider((ref) {
final assets = ref.watch(searchResultPageProvider).searchResult; final result = ref.watch(searchResultPageProvider);
return ref.watch(renderListProvider(assets)); return ref.watch(
renderListProviderWithGrouping(
(result.searchResult, result.isClip ? GroupAssetsBy.none : null),
),
);
}); });

View File

@ -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<Asset> 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,
);
},
);
}
}

View File

@ -2,17 +2,14 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/modules/search/providers/all_video_assets.provider.dart';
import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
class AllVideosPage extends HookConsumerWidget { class AllVideosPage extends HookConsumerWidget {
const AllVideosPage({super.key}); const AllVideosPage({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final videos = ref.watch(allVideoAssetsProvider);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('all_videos_page_title').tr(), title: const Text('all_videos_page_title').tr(),
@ -21,11 +18,7 @@ class AllVideosPage extends HookConsumerWidget {
icon: const Icon(Icons.arrow_back_ios_rounded), icon: const Icon(Icons.arrow_back_ios_rounded),
), ),
), ),
body: videos.widgetWhen( body: MultiselectGrid(renderListProvider: allVideoAssetsProvider),
onData: (assets) => ImmichAssetGrid(
assets: assets,
),
),
); );
} }
} }

View File

@ -1,14 +1,13 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; 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' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart'; 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/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/providers/people.provider.dart';
import 'package:immich_mobile/modules/search/ui/person_name_edit_form.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'; import 'package:immich_mobile/utils/image_url_builder.dart';
class PersonResultPage extends HookConsumerWidget { class PersonResultPage extends HookConsumerWidget {
@ -112,32 +111,30 @@ class PersonResultPage extends HookConsumerWidget {
), ),
], ],
), ),
body: ref.watch(personAssetsProvider(personId)).widgetWhen( body: MultiselectGrid(
onData: (renderList) => ImmichAssetGrid( renderListProvider: personAssetsProvider(personId),
renderList: renderList, topWidget: Padding(
topWidget: Padding( padding: const EdgeInsets.only(left: 8.0, top: 24),
padding: const EdgeInsets.only(left: 8.0, top: 24), child: Row(
child: Row( children: [
children: [ CircleAvatar(
CircleAvatar( radius: 36,
radius: 36, backgroundImage: NetworkImage(
backgroundImage: NetworkImage( getFaceThumbnailUrl(personId),
getFaceThumbnailUrl(personId), headers: {
headers: { "Authorization":
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}",
"Bearer ${isar_store.Store.get(isar_store.StoreKey.accessToken)}", },
},
),
),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: buildTitleBlock(),
),
],
), ),
), ),
), Padding(
padding: const EdgeInsets.only(left: 16.0),
child: buildTitleBlock(),
),
],
), ),
),
),
); );
} }
} }

View File

@ -4,11 +4,10 @@ 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/extensions/build_context_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/search_page_state.provider.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/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/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'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class SearchType { class SearchType {
@ -39,7 +38,6 @@ class SearchResultPage extends HookConsumerWidget {
final searchTermController = useTextEditingController(text: ""); final searchTermController = useTextEditingController(text: "");
final isNewSearch = useState(false); final isNewSearch = useState(false);
final currentSearchTerm = useState(searchTerm); final currentSearchTerm = useState(searchTerm);
final isDisplayDateGroup = useState(true);
FocusNode? searchFocusNode; FocusNode? searchFocusNode;
@ -48,9 +46,6 @@ class SearchResultPage extends HookConsumerWidget {
searchFocusNode = FocusNode(); searchFocusNode = FocusNode();
var searchType = _getSearchType(searchTerm); var searchType = _getSearchType(searchTerm);
searchType.isClip
? isDisplayDateGroup.value = false
: isDisplayDateGroup.value = true;
Future.delayed( Future.delayed(
Duration.zero, Duration.zero,
@ -63,18 +58,13 @@ class SearchResultPage extends HookConsumerWidget {
[], [],
); );
onSearchSubmitted(String newSearchTerm) { Future<void> onSearchSubmitted(String newSearchTerm) {
debugPrint("Re-Search with $newSearchTerm"); debugPrint("Re-Search with $newSearchTerm");
searchFocusNode?.unfocus(); searchFocusNode?.unfocus();
isNewSearch.value = false; isNewSearch.value = false;
currentSearchTerm.value = newSearchTerm; currentSearchTerm.value = newSearchTerm;
var searchType = _getSearchType(newSearchTerm); var searchType = _getSearchType(newSearchTerm);
searchType.isClip return ref
? isDisplayDateGroup.value = false
: isDisplayDateGroup.value = true;
ref
.watch(searchResultPageProvider.notifier) .watch(searchResultPageProvider.notifier)
.search(searchType.searchTerm, clipEnable: searchType.isClip); .search(searchType.searchTerm, clipEnable: searchType.isClip);
} }
@ -148,9 +138,10 @@ class SearchResultPage extends HookConsumerWidget {
); );
} }
Future<void> refresh() async => onSearchSubmitted(currentSearchTerm.value);
buildSearchResult() { buildSearchResult() {
var searchResultPageState = ref.watch(searchResultPageProvider); final searchResultPageState = ref.watch(searchResultPageProvider);
var allSearchAssets = ref.watch(searchResultPageProvider).searchResult;
if (searchResultPageState.isError) { if (searchResultPageState.isError) {
return Padding( return Padding(
@ -164,15 +155,15 @@ class SearchResultPage extends HookConsumerWidget {
} }
if (searchResultPageState.isSuccess) { if (searchResultPageState.isSuccess) {
if (isDisplayDateGroup.value) { return MultiselectGrid(
return ImmichAssetGrid( renderListProvider: searchRenderListProvider,
assets: allSearchAssets, archiveEnabled: true,
); deleteEnabled: true,
} else { editEnabled: true,
return SearchResultGrid( favoriteEnabled: true,
assets: allSearchAssets, stackEnabled: false,
); onRefresh: refresh,
} );
} }
return const SizedBox(); return const SizedBox();