diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 5190e2007f..4d72a9b0a5 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -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/selection_sliver_app_bar.dart'; -class _TimelineRestorationState extends ChangeNotifier { - 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 { +class Timeline extends StatelessWidget { const Timeline({ super.key, this.topSliverWidget, @@ -90,68 +59,38 @@ class Timeline extends StatefulWidget { final bool readOnly; final bool persistentBottomBar; - @override - State createState() => _TimelineState(); -} - -class _TimelineState extends State { - double? _lastWidth; - late final _TimelineRestorationState _restorationState; - - @override - void initState() { - super.initState(); - _restorationState = _TimelineRestorationState(); - } - - @override - void dispose() { - _restorationState.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, floatingActionButton: const DownloadStatusFloatingButton(), body: LayoutBuilder( - builder: (_, constraints) { - if (_lastWidth != null && _lastWidth != constraints.maxWidth) { - _restorationState.setShouldRestoreAssetPosition(true); - } - _lastWidth = constraints.maxWidth; - return _TimelineRestorationProvider( - notifier: _restorationState, - child: ProviderScope( - key: ValueKey(_lastWidth), - overrides: [ - 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, + builder: (_, constraints) => ProviderScope( + overrides: [ + timelineArgsProvider.overrideWith( + (ref) => TimelineArgs( + maxWidth: constraints.maxWidth, + maxHeight: constraints.maxHeight, + columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))), + showStorageIndicator: showStorageIndicator, + withStack: withStack, + groupBy: groupBy, ), ), - ); - }, + 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 { const _SliverTimeline({ - super.key, this.topSliverWidget, this.topSliverWidgetHeight, this.appBar, @@ -179,6 +117,7 @@ class _SliverTimeline extends ConsumerStatefulWidget { this.persistentBottomBar = false, this.snapToMonth = true, this.initialScrollOffset, + this.maxWidth, }); final Widget? topSliverWidget; @@ -189,6 +128,7 @@ class _SliverTimeline extends ConsumerStatefulWidget { final bool persistentBottomBar; final bool snapToMonth; final double? initialScrollOffset; + final double? maxWidth; @override ConsumerState createState() => _SliverTimelineState(); @@ -207,6 +147,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { int _perRow = 4; double _scaleFactor = 3.0; double _baseScaleFactor = 3.0; + int? _restoreAssetIndex; @override void initState() { @@ -225,6 +166,20 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { 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) { switch (event) { case ScrollToTopEvent(): @@ -242,21 +197,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { } } - void _onMultiSelectionToggled(_, bool isEnabled) { - EventStream.shared.emit(MultiSelectToggleEvent(isEnabled)); - } - void _restoreAssetPosition(_) { - final restorationState = _TimelineRestorationProvider.of(context); - if (!restorationState.shouldRestoreAssetPosition || restorationState.restoreAssetIndex == null) return; + if (_restoreAssetIndex == null) return; final asyncSegments = ref.read(timelineSegmentProvider); asyncSegments.whenData((segments) { - final targetSegment = segments.lastWhereOrNull( - (segment) => segment.firstAssetIndex <= restorationState.restoreAssetIndex!, - ); + final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _restoreAssetIndex!); if (targetSegment != null) { - final assetIndexInSegment = restorationState.restoreAssetIndex! - targetSegment.firstAssetIndex; + final assetIndexInSegment = _restoreAssetIndex! - targetSegment.firstAssetIndex; final newColumnCount = ref.read(timelineArgsProvider).columnCount; final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor(); 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 segments) { @@ -478,67 +430,56 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { return PrimaryScrollController( controller: _scrollController, - child: NotificationListener( - onNotification: (notification) { - final currentIndex = _getCurrentAssetIndex(segments); - if (currentIndex != null && mounted) { - _TimelineRestorationProvider.of(context).setRestoreAssetIndex(currentIndex); - } - return false; - }, - child: RawGestureDetector( - gestures: { - CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => CustomScaleGestureRecognizer(), - (CustomScaleGestureRecognizer scale) { - scale.onStart = (details) { - _baseScaleFactor = _scaleFactor; - }; + child: RawGestureDetector( + gestures: { + CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => CustomScaleGestureRecognizer(), + (CustomScaleGestureRecognizer scale) { + scale.onStart = (details) { + _baseScaleFactor = _scaleFactor; + }; - scale.onUpdate = (details) { - final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0); - final newPerRow = 7 - newScaleFactor.toInt(); + scale.onUpdate = (details) { + final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0); + final newPerRow = 7 - newScaleFactor.toInt(); + + if (newPerRow != _perRow) { final targetAssetIndex = _getCurrentAssetIndex(segments); + setState(() { + _scaleFactor = newScaleFactor; + _perRow = newPerRow; + _restoreAssetIndex = targetAssetIndex; + }); - if (newPerRow != _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); + ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow); + } + }; }, - child: Stack( - children: [ - timeline, - if (isMultiSelectStatusVisible) - Positioned( - top: MediaQuery.paddingOf(context).top, - left: 25, - child: const SizedBox( - height: kToolbarHeight, - child: Center(child: _MultiSelectStatusButton()), - ), + ), + }, + 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, + 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!, + ], ), ), ),