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

View File

@@ -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) { overrides: [
_restorationState.setShouldRestoreAssetPosition(true); timelineArgsProvider.overrideWith(
} (ref) => TimelineArgs(
_lastWidth = constraints.maxWidth; maxWidth: constraints.maxWidth,
return _TimelineRestorationProvider( maxHeight: constraints.maxHeight,
notifier: _restorationState, columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
child: ProviderScope( showStorageIndicator: showStorageIndicator,
key: ValueKey(_lastWidth), withStack: withStack,
overrides: [ groupBy: groupBy,
timelineArgsProvider.overrideWith(
(ref) => TimelineArgs(
maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight,
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
showStorageIndicator: widget.showStorageIndicator,
withStack: widget.withStack,
groupBy: widget.groupBy,
),
),
if (widget.readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()),
],
child: _SliverTimeline(
key: const ValueKey('_sliver_timeline'),
topSliverWidget: widget.topSliverWidget,
topSliverWidgetHeight: widget.topSliverWidgetHeight,
appBar: widget.appBar,
bottomSheet: widget.bottomSheet,
withScrubber: widget.withScrubber,
persistentBottomBar: widget.persistentBottomBar,
snapToMonth: widget.snapToMonth,
initialScrollOffset: widget.initialScrollOffset,
), ),
), ),
); if (readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()),
}, ],
child: _SliverTimeline(
topSliverWidget: topSliverWidget,
topSliverWidgetHeight: topSliverWidgetHeight,
appBar: appBar,
bottomSheet: bottomSheet,
withScrubber: withScrubber,
persistentBottomBar: persistentBottomBar,
snapToMonth: snapToMonth,
initialScrollOffset: 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,67 +430,56 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
return PrimaryScrollController( return PrimaryScrollController(
controller: _scrollController, controller: _scrollController,
child: NotificationListener<ScrollEndNotification>( child: RawGestureDetector(
onNotification: (notification) { gestures: {
final currentIndex = _getCurrentAssetIndex(segments); CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
if (currentIndex != null && mounted) { () => CustomScaleGestureRecognizer(),
_TimelineRestorationProvider.of(context).setRestoreAssetIndex(currentIndex); (CustomScaleGestureRecognizer scale) {
} scale.onStart = (details) {
return false; _baseScaleFactor = _scaleFactor;
}, };
child: RawGestureDetector(
gestures: {
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
() => CustomScaleGestureRecognizer(),
(CustomScaleGestureRecognizer scale) {
scale.onStart = (details) {
_baseScaleFactor = _scaleFactor;
};
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();
if (newPerRow != _perRow) {
final targetAssetIndex = _getCurrentAssetIndex(segments); final targetAssetIndex = _getCurrentAssetIndex(segments);
setState(() {
_scaleFactor = newScaleFactor;
_perRow = newPerRow;
_restoreAssetIndex = targetAssetIndex;
});
if (newPerRow != _perRow) { ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
final restorationState = _TimelineRestorationProvider.of(context); }
setState(() { };
_scaleFactor = newScaleFactor;
_perRow = newPerRow;
});
restorationState.setRestoreAssetIndex(targetAssetIndex);
restorationState.setShouldRestoreAssetPosition(true);
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
}
};
},
),
},
child: TimelineDragRegion(
onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null,
onAssetEnter: _handleDragAssetEnter,
onEnd: !isReadonlyModeEnabled ? _stopDrag : null,
onScroll: _dragScroll,
onScrollStart: () {
// Minimize the bottom sheet when drag selection starts
ref.read(timelineStateProvider.notifier).setScrolling(true);
}, },
child: Stack( ),
children: [ },
timeline, child: TimelineDragRegion(
if (isMultiSelectStatusVisible) onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null,
Positioned( onAssetEnter: _handleDragAssetEnter,
top: MediaQuery.paddingOf(context).top, onEnd: !isReadonlyModeEnabled ? _stopDrag : null,
left: 25, onScroll: _dragScroll,
child: const SizedBox( onScrollStart: () {
height: kToolbarHeight, // Minimize the bottom sheet when drag selection starts
child: Center(child: _MultiSelectStatusButton()), ref.read(timelineStateProvider.notifier).setScrolling(true);
), },
child: Stack(
children: [
timeline,
if (isBottomWidgetVisible)
Positioned(
top: MediaQuery.paddingOf(context).top,
left: 25,
child: const SizedBox(
height: kToolbarHeight,
child: Center(child: _MultiSelectStatusButton()),
), ),
if (isBottomWidgetVisible) widget.bottomSheet!, ),
], if (isBottomWidgetVisible) widget.bottomSheet!,
), ],
), ),
), ),
), ),