mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
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 <alex.tran1502@gmail.com>
This commit is contained in:
parent
6f3cb4f1bb
commit
b3342323de
5 changed files with 37 additions and 16 deletions
|
|
@ -3,27 +3,30 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
|
||||||
class SearchResult {
|
class SearchResult {
|
||||||
final List<BaseAsset> assets;
|
final List<BaseAsset> assets;
|
||||||
|
final double scrollOffset;
|
||||||
final int? nextPage;
|
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<BaseAsset>? assets, int? nextPage, double? scrollOffset}) {
|
||||||
|
return SearchResult(
|
||||||
SearchResult copyWith({List<BaseAsset>? assets, int? nextPage}) {
|
assets: assets ?? this.assets,
|
||||||
return SearchResult(assets: assets ?? this.assets, nextPage: nextPage ?? this.nextPage);
|
nextPage: nextPage ?? this.nextPage,
|
||||||
|
scrollOffset: scrollOffset ?? this.scrollOffset,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SearchResult(assets: $assets, nextPage: $nextPage)';
|
String toString() => 'SearchResult(assets: ${assets.length}, nextPage: $nextPage, scrollOffset: $scrollOffset)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(covariant SearchResult other) {
|
bool operator ==(covariant SearchResult other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
final listEquals = const DeepCollectionEquality().equals;
|
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
|
@override
|
||||||
int get hashCode => assets.hashCode ^ nextPage.hashCode;
|
int get hashCode => assets.hashCode ^ nextPage.hashCode ^ scrollOffset.hashCode;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ class TimelineService {
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
await _bucketSubscription?.cancel();
|
await _bucketSubscription?.cancel();
|
||||||
_bucketSubscription = null;
|
_bucketSubscription = null;
|
||||||
_buffer.clear();
|
_buffer = [];
|
||||||
_bufferOffset = 0;
|
_bufferOffset = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -599,9 +599,9 @@ class _SearchResultGrid extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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();
|
return const _SearchEmptyContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -615,6 +615,7 @@ class _SearchResultGrid extends ConsumerWidget {
|
||||||
|
|
||||||
if (metrics.pixels >= metrics.maxScrollExtent && isVerticalScroll && !isBottomSheetNotification) {
|
if (metrics.pixels >= metrics.maxScrollExtent && isVerticalScroll && !isBottomSheetNotification) {
|
||||||
onScrollEnd();
|
onScrollEnd();
|
||||||
|
ref.read(paginatedSearchProvider.notifier).setScrollOffset(metrics.maxScrollExtent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -623,17 +624,18 @@ class _SearchResultGrid extends ConsumerWidget {
|
||||||
child: ProviderScope(
|
child: ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
timelineServiceProvider.overrideWith((ref) {
|
timelineServiceProvider.overrideWith((ref) {
|
||||||
final timelineService = ref.watch(timelineFactoryProvider).fromAssets(searchResult.assets);
|
final timelineService = ref.watch(timelineFactoryProvider).fromAssets(assets);
|
||||||
ref.onDispose(timelineService.dispose);
|
ref.onDispose(timelineService.dispose);
|
||||||
return timelineService;
|
return timelineService;
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
child: Timeline(
|
child: Timeline(
|
||||||
key: ValueKey(searchResult.totalAssets),
|
key: ValueKey(assets.length),
|
||||||
groupBy: GroupAssetsBy.none,
|
groupBy: GroupAssetsBy.none,
|
||||||
appBar: null,
|
appBar: null,
|
||||||
bottomSheet: const GeneralBottomSheet(minChildSize: 0.20),
|
bottomSheet: const GeneralBottomSheet(minChildSize: 0.20),
|
||||||
snapToMonth: false,
|
snapToMonth: false,
|
||||||
|
initialScrollOffset: ref.read(paginatedSearchProvider.select((s) => s.scrollOffset)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,20 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setScrollOffset(double offset) {
|
||||||
|
state = state.copyWith(scrollOffset: offset);
|
||||||
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
state = const SearchResult(assets: [], nextPage: 1);
|
state = const SearchResult(assets: [], nextPage: 1, scrollOffset: 0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ class Timeline extends StatelessWidget {
|
||||||
this.groupBy,
|
this.groupBy,
|
||||||
this.withScrubber = true,
|
this.withScrubber = true,
|
||||||
this.snapToMonth = true,
|
this.snapToMonth = true,
|
||||||
|
this.initialScrollOffset,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
|
|
@ -51,6 +52,7 @@ class Timeline extends StatelessWidget {
|
||||||
final GroupAssetsBy? groupBy;
|
final GroupAssetsBy? groupBy;
|
||||||
final bool withScrubber;
|
final bool withScrubber;
|
||||||
final bool snapToMonth;
|
final bool snapToMonth;
|
||||||
|
final double? initialScrollOffset;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -78,6 +80,7 @@ class Timeline extends StatelessWidget {
|
||||||
bottomSheet: bottomSheet,
|
bottomSheet: bottomSheet,
|
||||||
withScrubber: withScrubber,
|
withScrubber: withScrubber,
|
||||||
snapToMonth: snapToMonth,
|
snapToMonth: snapToMonth,
|
||||||
|
initialScrollOffset: initialScrollOffset,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -93,6 +96,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
||||||
this.bottomSheet,
|
this.bottomSheet,
|
||||||
this.withScrubber = true,
|
this.withScrubber = true,
|
||||||
this.snapToMonth = true,
|
this.snapToMonth = true,
|
||||||
|
this.initialScrollOffset,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
|
|
@ -101,6 +105,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
||||||
final Widget? bottomSheet;
|
final Widget? bottomSheet;
|
||||||
final bool withScrubber;
|
final bool withScrubber;
|
||||||
final bool snapToMonth;
|
final bool snapToMonth;
|
||||||
|
final double? initialScrollOffset;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState createState() => _SliverTimelineState();
|
ConsumerState createState() => _SliverTimelineState();
|
||||||
|
|
@ -124,7 +129,10 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_scrollController = ScrollController(onAttach: _restoreScalePosition);
|
_scrollController = ScrollController(
|
||||||
|
initialScrollOffset: widget.initialScrollOffset ?? 0.0,
|
||||||
|
onAttach: _restoreScalePosition,
|
||||||
|
);
|
||||||
_eventSubscription = EventStream.shared.listen(_onEvent);
|
_eventSubscription = EventStream.shared.listen(_onEvent);
|
||||||
|
|
||||||
final currentTilesPerRow = ref.read(settingsProvider).get(Setting.tilesPerRow);
|
final currentTilesPerRow = ref.read(settingsProvider).get(Setting.tilesPerRow);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue