feat(mobile): keep search results visible (#26498)

Search results are replaced with a spinner when loading the next page,
which is quite jarring. Search results now remain visible when loading
the next page with a spinner at the bottom. The next page also loads
sooner, which makes it feel a lot smoother.

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Thomas
2026-03-04 17:27:11 +00:00
committed by GitHub
parent 7e9da945f6
commit 228ac63ab9
8 changed files with 180 additions and 110 deletions

View File

@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/search_result.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/search.service.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/providers/infrastructure/search.provider.dart';
@@ -21,40 +23,52 @@ class SearchFilterProvider extends Notifier<SearchFilter?> {
}
}
final paginatedSearchProvider = StateNotifierProvider<PaginatedSearchNotifier, SearchResult>(
class SearchState {
final List<BaseAsset> assets;
final int? nextPage;
final bool isLoading;
const SearchState({this.assets = const [], this.nextPage = 1, this.isLoading = false});
}
final paginatedSearchProvider = StateNotifierProvider<PaginatedSearchNotifier, SearchState>(
(ref) => PaginatedSearchNotifier(ref.watch(searchServiceProvider)),
);
class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
class PaginatedSearchNotifier extends StateNotifier<SearchState> {
final SearchService _searchService;
final _assetCountController = StreamController<int>.broadcast();
PaginatedSearchNotifier(this._searchService) : super(const SearchResult(assets: [], nextPage: 1));
PaginatedSearchNotifier(this._searchService) : super(const SearchState());
Future<bool> search(SearchFilter filter) async {
if (state.nextPage == null) {
return false;
}
Stream<int> get assetCount => _assetCountController.stream;
Future<void> search(SearchFilter filter) async {
if (state.nextPage == null || state.isLoading) return;
state = SearchState(assets: state.assets, nextPage: state.nextPage, isLoading: true);
final result = await _searchService.search(filter, state.nextPage!);
if (result == null) {
return false;
state = SearchState(assets: state.assets, nextPage: state.nextPage);
return;
}
state = SearchResult(
assets: [...state.assets, ...result.assets],
nextPage: result.nextPage,
scrollOffset: state.scrollOffset,
);
final assets = [...state.assets, ...result.assets];
state = SearchState(assets: assets, nextPage: result.nextPage);
return true;
_assetCountController.add(assets.length);
}
void setScrollOffset(double offset) {
state = state.copyWith(scrollOffset: offset);
void clear() {
state = const SearchState();
_assetCountController.add(0);
}
clear() {
state = const SearchResult(assets: [], nextPage: 1, scrollOffset: 0.0);
@override
void dispose() {
_assetCountController.close();
super.dispose();
}
}