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:
parent
ad09896f58
commit
56cde0438c
@ -169,4 +169,4 @@ SPEC CHECKSUMS:
|
|||||||
|
|
||||||
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
|
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,
|
final renderListQueryProvider = StreamProvider.family<RenderList,
|
||||||
QueryBuilder<Asset, Asset, QAfterSortBy>?>(
|
QueryBuilder<Asset, Asset, QAfterSortBy>?>(
|
||||||
(ref, query) =>
|
(ref, query) =>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
@ -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: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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
Loading…
Reference in New Issue
Block a user