1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-24 08:52:28 +02:00

fix(mobile): search page (#13833)

* fix(mobile): search page minor problems

* fix: flashing between search

* restore search size

* remove print statement

* linting
This commit is contained in:
Alex 2024-10-30 14:27:13 -05:00 committed by GitHub
parent 9d75c5b999
commit 318ab756cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 140 additions and 94 deletions

View File

@ -0,0 +1,37 @@
import 'package:collection/collection.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
class SearchResult {
final List<Asset> assets;
final int? nextPage;
SearchResult({
required this.assets,
this.nextPage,
});
SearchResult copyWith({
List<Asset>? assets,
int? nextPage,
}) {
return SearchResult(
assets: assets ?? this.assets,
nextPage: nextPage ?? this.nextPage,
);
}
@override
String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)';
@override
bool operator ==(covariant SearchResult other) {
if (identical(this, other)) return true;
final listEquals = const DeepCollectionEquality().equals;
return listEquals(other.assets, assets) && other.nextPage == nextPage;
}
@override
int get hashCode => assets.hashCode ^ nextPage.hashCode;
}

View File

@ -58,23 +58,22 @@ class SearchPage extends HookConsumerWidget {
final mediaTypeCurrentFilterWidget = useState<Widget?>(null); final mediaTypeCurrentFilterWidget = useState<Widget?>(null);
final displayOptionCurrentFilterWidget = useState<Widget?>(null); final displayOptionCurrentFilterWidget = useState<Widget?>(null);
final currentPage = useState(1); final isSearching = useState(false);
final searchProvider = ref.watch(paginatedSearchProvider);
final searchResultCount = useState(0);
search() async { search() async {
if (prefilter == null && filter.value == previousFilter.value) return; if (prefilter == null && filter.value == previousFilter.value) return;
isSearching.value = true;
ref.watch(paginatedSearchProvider.notifier).clear(); ref.watch(paginatedSearchProvider.notifier).clear();
await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
currentPage.value = 1;
final searchResult = await ref
.watch(paginatedSearchProvider.notifier)
.getNextPage(filter.value, currentPage.value);
previousFilter.value = filter.value; previousFilter.value = filter.value;
searchResultCount.value = searchResult.length; isSearching.value = false;
}
loadMoreSearchResult() async {
isSearching.value = true;
await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
isSearching.value = false;
} }
searchPrefilter() { searchPrefilter() {
@ -97,20 +96,16 @@ class SearchPage extends HookConsumerWidget {
useEffect( useEffect(
() { () {
Future.microtask(
() => ref.invalidate(paginatedSearchProvider),
);
searchPrefilter(); searchPrefilter();
return null; return null;
}, },
[], [],
); );
loadMoreSearchResult() async {
currentPage.value += 1;
final searchResult = await ref
.watch(paginatedSearchProvider.notifier)
.getNextPage(filter.value, currentPage.value);
searchResultCount.value = searchResult.length;
}
showPeoplePicker() { showPeoplePicker() {
handleOnSelect(Set<Person> value) { handleOnSelect(Set<Person> value) {
filter.value = filter.value.copyWith( filter.value = filter.value.copyWith(
@ -465,41 +460,6 @@ class SearchPage extends HookConsumerWidget {
search(); search();
} }
buildSearchResult() {
return switch (searchProvider) {
AsyncData() => Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: NotificationListener<ScrollEndNotification>(
onNotification: (notification) {
final metrics = notification.metrics;
final shouldLoadMore = searchResultCount.value > 75;
if (metrics.pixels >= metrics.maxScrollExtent &&
shouldLoadMore) {
loadMoreSearchResult();
}
return true;
},
child: MultiselectGrid(
renderListProvider: paginatedSearchRenderListProvider,
archiveEnabled: true,
deleteEnabled: true,
editEnabled: true,
favoriteEnabled: true,
stackEnabled: false,
emptyIndicator: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SearchEmptyContent(),
),
),
),
),
),
AsyncError(:final error) => Text('Error: $error'),
_ => const Expanded(child: Center(child: CircularProgressIndicator())),
};
}
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
appBar: AppBar( appBar: AppBar(
@ -635,13 +595,67 @@ class SearchPage extends HookConsumerWidget {
), ),
), ),
), ),
buildSearchResult(), SearchResultGrid(
onScrollEnd: loadMoreSearchResult,
isSearching: isSearching.value,
),
], ],
), ),
); );
} }
} }
class SearchResultGrid extends StatelessWidget {
final VoidCallback onScrollEnd;
final bool isSearching;
const SearchResultGrid({
super.key,
required this.onScrollEnd,
this.isSearching = false,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: NotificationListener<ScrollEndNotification>(
onNotification: (notification) {
final isBottomSheetNotification = notification.context
?.findAncestorWidgetOfExactType<
DraggableScrollableSheet>() !=
null;
final metrics = notification.metrics;
final isVerticalScroll = metrics.axis == Axis.vertical;
if (metrics.pixels >= metrics.maxScrollExtent &&
isVerticalScroll &&
!isBottomSheetNotification) {
onScrollEnd();
}
return true;
},
child: MultiselectGrid(
renderListProvider: paginatedSearchRenderListProvider,
archiveEnabled: true,
deleteEnabled: true,
editEnabled: true,
favoriteEnabled: true,
stackEnabled: false,
emptyIndicator: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: !isSearching ? SearchEmptyContent() : SizedBox.shrink(),
),
),
),
),
);
}
}
class SearchEmptyContent extends StatelessWidget { class SearchEmptyContent extends StatelessWidget {
const SearchEmptyContent({super.key}); const SearchEmptyContent({super.key});

View File

@ -1,46 +1,39 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/search/search_result.model.dart';
import 'package:immich_mobile/providers/asset_viewer/render_list.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/render_list.provider.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/services/search.service.dart'; import 'package:immich_mobile/services/search.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'paginated_search.provider.g.dart'; part 'paginated_search.provider.g.dart';
@riverpod final paginatedSearchProvider =
class PaginatedSearch extends _$PaginatedSearch { StateNotifierProvider<PaginatedSearchNotifier, SearchResult>(
Future<List<Asset>?> _search(SearchFilter filter, int page) async { (ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)),
final service = ref.read(searchServiceProvider); );
final result = await service.search(filter, page);
return result; class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
} final SearchService _searchService;
@override PaginatedSearchNotifier(this._searchService)
Future<List<Asset>> build() async { : super(SearchResult(assets: [], nextPage: 1));
return [];
}
Future<List<Asset>> getNextPage(SearchFilter filter, int nextPage) async { search(SearchFilter filter) async {
state = const AsyncValue.loading(); if (state.nextPage == null) return;
final newState = await AsyncValue.guard(() async { final result = await _searchService.search(filter, state.nextPage!);
final assets = await _search(filter, nextPage);
if (assets != null) { if (result == null) return;
return [...?state.value, ...assets];
}
});
state = newState.valueOrNull == null state = SearchResult(
? const AsyncValue.data([]) assets: [...state.assets, ...result.assets],
: AsyncValue.data(newState.value!); nextPage: result.nextPage,
);
return newState.valueOrNull ?? [];
} }
clear() { clear() {
state = const AsyncValue.data([]); state = SearchResult(assets: [], nextPage: 1);
} }
} }
@ -48,15 +41,11 @@ class PaginatedSearch extends _$PaginatedSearch {
AsyncValue<RenderList> paginatedSearchRenderList( AsyncValue<RenderList> paginatedSearchRenderList(
PaginatedSearchRenderListRef ref, PaginatedSearchRenderListRef ref,
) { ) {
final assets = ref.watch(paginatedSearchProvider).value; final result = ref.watch(paginatedSearchProvider);
if (assets != null) { return ref.watch(
return ref.watch( renderListProviderWithGrouping(
renderListProviderWithGrouping( (result.assets, GroupAssetsBy.none),
(assets, GroupAssetsBy.none), ),
), );
);
} else {
return const AsyncValue.loading();
}
} }

View File

@ -1,8 +1,10 @@
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/string_extensions.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/models/search/search_result.model.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
@ -44,7 +46,7 @@ class SearchService {
} }
} }
Future<List<Asset>?> search(SearchFilter filter, int page) async { Future<SearchResult?> search(SearchFilter filter, int page) async {
try { try {
SearchResponseDto? response; SearchResponseDto? response;
AssetTypeEnum? type; AssetTypeEnum? type;
@ -103,8 +105,12 @@ class SearchService {
return null; return null;
} }
return _assetRepository return SearchResult(
.getAllByRemoteId(response.assets.items.map((e) => e.id)); assets: await _assetRepository.getAllByRemoteId(
response.assets.items.map((e) => e.id),
),
nextPage: response.assets.nextPage?.toInt(),
);
} catch (error, stackTrace) { } catch (error, stackTrace) {
_log.severe("Failed to search for assets", error, stackTrace); _log.severe("Failed to search for assets", error, stackTrace);
} }