fix: simplify timeline rebuild on orientation (#26408)

* revert: current fix

# Conflicts:
#	mobile/lib/presentation/widgets/timeline/timeline.widget.dart

* fix: simpler fix

* rebase

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong
2026-02-24 00:00:09 +05:30
committed by GitHub
parent 367025a3a8
commit 4f39663d27
@@ -29,38 +29,7 @@ import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
class _TimelineRestorationState extends ChangeNotifier { class Timeline extends StatelessWidget {
int? _restoreAssetIndex;
bool _shouldRestoreAssetPosition = false;
int? get restoreAssetIndex => _restoreAssetIndex;
bool get shouldRestoreAssetPosition => _shouldRestoreAssetPosition;
void setRestoreAssetIndex(int? index) {
_restoreAssetIndex = index;
notifyListeners();
}
void setShouldRestoreAssetPosition(bool should) {
_shouldRestoreAssetPosition = should;
notifyListeners();
}
void clearRestoreAssetIndex() {
_restoreAssetIndex = null;
notifyListeners();
}
}
class _TimelineRestorationProvider extends InheritedNotifier<_TimelineRestorationState> {
const _TimelineRestorationProvider({required super.notifier, required super.child});
static _TimelineRestorationState of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_TimelineRestorationProvider>()!.notifier!;
}
}
class Timeline extends StatefulWidget {
const Timeline({ const Timeline({
super.key, super.key,
this.topSliverWidget, this.topSliverWidget,
@@ -90,68 +59,38 @@ class Timeline extends StatefulWidget {
final bool readOnly; final bool readOnly;
final bool persistentBottomBar; final bool persistentBottomBar;
@override
State<Timeline> createState() => _TimelineState();
}
class _TimelineState extends State<Timeline> {
double? _lastWidth;
late final _TimelineRestorationState _restorationState;
@override
void initState() {
super.initState();
_restorationState = _TimelineRestorationState();
}
@override
void dispose() {
_restorationState.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
floatingActionButton: const DownloadStatusFloatingButton(), floatingActionButton: const DownloadStatusFloatingButton(),
body: LayoutBuilder( body: LayoutBuilder(
builder: (_, constraints) { builder: (_, constraints) => ProviderScope(
if (_lastWidth != null && _lastWidth != constraints.maxWidth) {
_restorationState.setShouldRestoreAssetPosition(true);
}
_lastWidth = constraints.maxWidth;
return _TimelineRestorationProvider(
notifier: _restorationState,
child: ProviderScope(
key: ValueKey(_lastWidth),
overrides: [ overrides: [
timelineArgsProvider.overrideWith( timelineArgsProvider.overrideWith(
(ref) => TimelineArgs( (ref) => TimelineArgs(
maxWidth: constraints.maxWidth, maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight, maxHeight: constraints.maxHeight,
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))), columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
showStorageIndicator: widget.showStorageIndicator, showStorageIndicator: showStorageIndicator,
withStack: widget.withStack, withStack: withStack,
groupBy: widget.groupBy, groupBy: groupBy,
), ),
), ),
if (widget.readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()), if (readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()),
], ],
child: _SliverTimeline( child: _SliverTimeline(
key: const ValueKey('_sliver_timeline'), topSliverWidget: topSliverWidget,
topSliverWidget: widget.topSliverWidget, topSliverWidgetHeight: topSliverWidgetHeight,
topSliverWidgetHeight: widget.topSliverWidgetHeight, appBar: appBar,
appBar: widget.appBar, bottomSheet: bottomSheet,
bottomSheet: widget.bottomSheet, withScrubber: withScrubber,
withScrubber: widget.withScrubber, persistentBottomBar: persistentBottomBar,
persistentBottomBar: widget.persistentBottomBar, snapToMonth: snapToMonth,
snapToMonth: widget.snapToMonth, initialScrollOffset: initialScrollOffset,
initialScrollOffset: widget.initialScrollOffset, maxWidth: constraints.maxWidth,
), ),
), ),
);
},
), ),
); );
} }
@@ -170,7 +109,6 @@ class _AlwaysReadOnlyNotifier extends ReadOnlyModeNotifier {
class _SliverTimeline extends ConsumerStatefulWidget { class _SliverTimeline extends ConsumerStatefulWidget {
const _SliverTimeline({ const _SliverTimeline({
super.key,
this.topSliverWidget, this.topSliverWidget,
this.topSliverWidgetHeight, this.topSliverWidgetHeight,
this.appBar, this.appBar,
@@ -179,6 +117,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
this.persistentBottomBar = false, this.persistentBottomBar = false,
this.snapToMonth = true, this.snapToMonth = true,
this.initialScrollOffset, this.initialScrollOffset,
this.maxWidth,
}); });
final Widget? topSliverWidget; final Widget? topSliverWidget;
@@ -189,6 +128,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
final bool persistentBottomBar; final bool persistentBottomBar;
final bool snapToMonth; final bool snapToMonth;
final double? initialScrollOffset; final double? initialScrollOffset;
final double? maxWidth;
@override @override
ConsumerState createState() => _SliverTimelineState(); ConsumerState createState() => _SliverTimelineState();
@@ -207,6 +147,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
int _perRow = 4; int _perRow = 4;
double _scaleFactor = 3.0; double _scaleFactor = 3.0;
double _baseScaleFactor = 3.0; double _baseScaleFactor = 3.0;
int? _restoreAssetIndex;
@override @override
void initState() { void initState() {
@@ -225,6 +166,20 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled); ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled);
} }
@override
void didUpdateWidget(covariant _SliverTimeline oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.maxWidth != oldWidget.maxWidth) {
final asyncSegments = ref.read(timelineSegmentProvider);
asyncSegments.whenData((segments) {
final index = _getCurrentAssetIndex(segments);
// Refresh to wait for new segments to be generated with the updated width before restoring the scroll position
final _ = ref.refresh(timelineArgsProvider);
_restoreAssetIndex = index;
});
}
}
void _onEvent(Event event) { void _onEvent(Event event) {
switch (event) { switch (event) {
case ScrollToTopEvent(): case ScrollToTopEvent():
@@ -242,21 +197,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
} }
} }
void _onMultiSelectionToggled(_, bool isEnabled) {
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
}
void _restoreAssetPosition(_) { void _restoreAssetPosition(_) {
final restorationState = _TimelineRestorationProvider.of(context); if (_restoreAssetIndex == null) return;
if (!restorationState.shouldRestoreAssetPosition || restorationState.restoreAssetIndex == null) return;
final asyncSegments = ref.read(timelineSegmentProvider); final asyncSegments = ref.read(timelineSegmentProvider);
asyncSegments.whenData((segments) { asyncSegments.whenData((segments) {
final targetSegment = segments.lastWhereOrNull( final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _restoreAssetIndex!);
(segment) => segment.firstAssetIndex <= restorationState.restoreAssetIndex!,
);
if (targetSegment != null) { if (targetSegment != null) {
final assetIndexInSegment = restorationState.restoreAssetIndex! - targetSegment.firstAssetIndex; final assetIndexInSegment = _restoreAssetIndex! - targetSegment.firstAssetIndex;
final newColumnCount = ref.read(timelineArgsProvider).columnCount; final newColumnCount = ref.read(timelineArgsProvider).columnCount;
final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor(); final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor();
final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment; final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment;
@@ -268,7 +216,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
}); });
} }
}); });
restorationState.clearRestoreAssetIndex(); _restoreAssetIndex = null;
}
void _onMultiSelectionToggled(_, bool isEnabled) {
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
} }
int? _getCurrentAssetIndex(List<Segment> segments) { int? _getCurrentAssetIndex(List<Segment> segments) {
@@ -478,14 +430,6 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
return PrimaryScrollController( return PrimaryScrollController(
controller: _scrollController, controller: _scrollController,
child: NotificationListener<ScrollEndNotification>(
onNotification: (notification) {
final currentIndex = _getCurrentAssetIndex(segments);
if (currentIndex != null && mounted) {
_TimelineRestorationProvider.of(context).setRestoreAssetIndex(currentIndex);
}
return false;
},
child: RawGestureDetector( child: RawGestureDetector(
gestures: { gestures: {
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>( CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
@@ -498,17 +442,15 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
scale.onUpdate = (details) { scale.onUpdate = (details) {
final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0); final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0);
final newPerRow = 7 - newScaleFactor.toInt(); final newPerRow = 7 - newScaleFactor.toInt();
final targetAssetIndex = _getCurrentAssetIndex(segments);
if (newPerRow != _perRow) { if (newPerRow != _perRow) {
final restorationState = _TimelineRestorationProvider.of(context); final targetAssetIndex = _getCurrentAssetIndex(segments);
setState(() { setState(() {
_scaleFactor = newScaleFactor; _scaleFactor = newScaleFactor;
_perRow = newPerRow; _perRow = newPerRow;
_restoreAssetIndex = targetAssetIndex;
}); });
restorationState.setRestoreAssetIndex(targetAssetIndex);
restorationState.setShouldRestoreAssetPosition(true);
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow); ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
} }
}; };
@@ -527,7 +469,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
child: Stack( child: Stack(
children: [ children: [
timeline, timeline,
if (isMultiSelectStatusVisible) if (isBottomWidgetVisible)
Positioned( Positioned(
top: MediaQuery.paddingOf(context).top, top: MediaQuery.paddingOf(context).top,
left: 25, left: 25,
@@ -541,7 +483,6 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
), ),
), ),
), ),
),
); );
}, },
), ),