mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00: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:
parent
ad09896f58
commit
56cde0438c
9 changed files with 78 additions and 105 deletions
|
@ -169,4 +169,4 @@ SPEC CHECKSUMS:
|
|||
|
||||
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.11.3
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue