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
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,
QueryBuilder<Asset, Asset, QAfterSortBy>?>(
(ref, query) =>

View File

@ -5,12 +5,14 @@ class SearchResultPageState {
final bool isLoading;
final bool isSuccess;
final bool isError;
final bool isClip;
final List<Asset> 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<Asset>? 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;
}
}

View File

@ -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<List<Asset>>((ref) async {
return ref
final allVideoAssetsProvider = StreamProvider<RenderList>((ref) {
final query = ref
.watch(dbProvider)
.assets
.filter()
.isArchivedEqualTo(false)
.isTrashedEqualTo(false)
.typeEqualTo(AssetType.video)
.findAll();
.sortByFileCreatedAtDesc();
return renderListGenerator(query, ref);
});

View File

@ -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<SearchResultPageState> {
isError: false,
isLoading: true,
isSuccess: false,
isClip: false,
),
);
final SearchService _searchService;
void search(String searchTerm, {bool clipEnable = true}) async {
Future<void> search(String searchTerm, {bool clipEnable = true}) async {
state = state.copyWith(
searchResult: [],
isError: false,
@ -37,6 +39,7 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
isError: false,
isLoading: false,
isSuccess: true,
isClip: clipEnable,
);
} else {
state = state.copyWith(
@ -44,6 +47,7 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
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),
),
);
});

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

View File

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

View File

@ -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<void> 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<void> 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();