From b7bbff6db10565d75039c83f0694491a6175034c Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:30:51 +0530 Subject: [PATCH] fix: persist search page scroll offset between rebuilds (#22733) fix: persist search scroll between rebuilds Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex --- .../domain/models/search_result.model.dart | 19 +++++++++++-------- .../lib/domain/services/timeline.service.dart | 2 +- .../pages/search/drift_search.page.dart | 10 ++++++---- .../search/paginated_search.provider.dart | 12 ++++++++++-- .../widgets/timeline/timeline.widget.dart | 10 +++++++++- 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/mobile/lib/domain/models/search_result.model.dart b/mobile/lib/domain/models/search_result.model.dart index bae8b8e821..947bc6192f 100644 --- a/mobile/lib/domain/models/search_result.model.dart +++ b/mobile/lib/domain/models/search_result.model.dart @@ -3,27 +3,30 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; class SearchResult { final List assets; + final double scrollOffset; final int? nextPage; - const SearchResult({required this.assets, this.nextPage}); + const SearchResult({required this.assets, this.scrollOffset = 0.0, this.nextPage}); - int get totalAssets => assets.length; - - SearchResult copyWith({List? assets, int? nextPage}) { - return SearchResult(assets: assets ?? this.assets, nextPage: nextPage ?? this.nextPage); + SearchResult copyWith({List? assets, int? nextPage, double? scrollOffset}) { + return SearchResult( + assets: assets ?? this.assets, + nextPage: nextPage ?? this.nextPage, + scrollOffset: scrollOffset ?? this.scrollOffset, + ); } @override - String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)'; + String toString() => 'SearchResult(assets: ${assets.length}, nextPage: $nextPage, scrollOffset: $scrollOffset)'; @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; + return listEquals(other.assets, assets) && other.nextPage == nextPage && other.scrollOffset == scrollOffset; } @override - int get hashCode => assets.hashCode ^ nextPage.hashCode; + int get hashCode => assets.hashCode ^ nextPage.hashCode ^ scrollOffset.hashCode; } diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index bf354847c3..6a7a1a22b2 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -203,7 +203,7 @@ class TimelineService { Future dispose() async { await _bucketSubscription?.cancel(); _bucketSubscription = null; - _buffer.clear(); + _buffer = []; _bufferOffset = 0; } } diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 7e70ebf8ff..92716144f3 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -599,9 +599,9 @@ class _SearchResultGrid extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final searchResult = ref.watch(paginatedSearchProvider); + final assets = ref.watch(paginatedSearchProvider.select((s) => s.assets)); - if (searchResult.totalAssets == 0) { + if (assets.isEmpty) { return const _SearchEmptyContent(); } @@ -615,6 +615,7 @@ class _SearchResultGrid extends ConsumerWidget { if (metrics.pixels >= metrics.maxScrollExtent && isVerticalScroll && !isBottomSheetNotification) { onScrollEnd(); + ref.read(paginatedSearchProvider.notifier).setScrollOffset(metrics.maxScrollExtent); } return true; @@ -623,17 +624,18 @@ class _SearchResultGrid extends ConsumerWidget { child: ProviderScope( overrides: [ timelineServiceProvider.overrideWith((ref) { - final timelineService = ref.watch(timelineFactoryProvider).fromAssets(searchResult.assets); + final timelineService = ref.watch(timelineFactoryProvider).fromAssets(assets); ref.onDispose(timelineService.dispose); return timelineService; }), ], child: Timeline( - key: ValueKey(searchResult.totalAssets), + key: ValueKey(assets.length), groupBy: GroupAssetsBy.none, appBar: null, bottomSheet: const GeneralBottomSheet(minChildSize: 0.20), snapToMonth: false, + initialScrollOffset: ref.read(paginatedSearchProvider.select((s) => s.scrollOffset)), ), ), ), diff --git a/mobile/lib/presentation/pages/search/paginated_search.provider.dart b/mobile/lib/presentation/pages/search/paginated_search.provider.dart index 718a241ba4..c0c822198d 100644 --- a/mobile/lib/presentation/pages/search/paginated_search.provider.dart +++ b/mobile/lib/presentation/pages/search/paginated_search.provider.dart @@ -24,12 +24,20 @@ class PaginatedSearchNotifier extends StateNotifier { return false; } - state = SearchResult(assets: [...state.assets, ...result.assets], nextPage: result.nextPage); + state = SearchResult( + assets: [...state.assets, ...result.assets], + nextPage: result.nextPage, + scrollOffset: state.scrollOffset, + ); return true; } + void setScrollOffset(double offset) { + state = state.copyWith(scrollOffset: offset); + } + clear() { - state = const SearchResult(assets: [], nextPage: 1); + state = const SearchResult(assets: [], nextPage: 1, scrollOffset: 0.0); } } diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index e8832173a1..5f1e7f27b0 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -40,6 +40,7 @@ class Timeline extends StatelessWidget { this.groupBy, this.withScrubber = true, this.snapToMonth = true, + this.initialScrollOffset, }); final Widget? topSliverWidget; @@ -51,6 +52,7 @@ class Timeline extends StatelessWidget { final GroupAssetsBy? groupBy; final bool withScrubber; final bool snapToMonth; + final double? initialScrollOffset; @override Widget build(BuildContext context) { @@ -78,6 +80,7 @@ class Timeline extends StatelessWidget { bottomSheet: bottomSheet, withScrubber: withScrubber, snapToMonth: snapToMonth, + initialScrollOffset: initialScrollOffset, ), ), ), @@ -93,6 +96,7 @@ class _SliverTimeline extends ConsumerStatefulWidget { this.bottomSheet, this.withScrubber = true, this.snapToMonth = true, + this.initialScrollOffset, }); final Widget? topSliverWidget; @@ -101,6 +105,7 @@ class _SliverTimeline extends ConsumerStatefulWidget { final Widget? bottomSheet; final bool withScrubber; final bool snapToMonth; + final double? initialScrollOffset; @override ConsumerState createState() => _SliverTimelineState(); @@ -124,7 +129,10 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { @override void initState() { super.initState(); - _scrollController = ScrollController(onAttach: _restoreScalePosition); + _scrollController = ScrollController( + initialScrollOffset: widget.initialScrollOffset ?? 0.0, + onAttach: _restoreScalePosition, + ); _eventSubscription = EventStream.shared.listen(_onEvent); final currentTilesPerRow = ref.read(settingsProvider).get(Setting.tilesPerRow);