diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index ac20e73190..da0497539b 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -29,7 +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 Timeline extends StatelessWidget { +class Timeline extends ConsumerWidget { const Timeline({ super.key, this.topSliverWidget, @@ -58,15 +58,15 @@ class Timeline extends StatelessWidget { final bool readOnly; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Scaffold( resizeToAvoidBottomInset: false, floatingActionButton: const DownloadStatusFloatingButton(), body: LayoutBuilder( builder: (_, constraints) => ProviderScope( overrides: [ - timelineArgsProvider.overrideWith( - (ref) => TimelineArgs( + timelineArgsProvider.overrideWithValue( + TimelineArgs( maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight, columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))), @@ -78,6 +78,7 @@ class Timeline extends StatelessWidget { if (readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()), ], child: _SliverTimeline( + key: const ValueKey('_sliver_timeline'), topSliverWidget: topSliverWidget, topSliverWidgetHeight: topSliverWidgetHeight, appBar: appBar, @@ -105,6 +106,7 @@ class _AlwaysReadOnlyNotifier extends ReadOnlyModeNotifier { class _SliverTimeline extends ConsumerStatefulWidget { const _SliverTimeline({ + super.key, this.topSliverWidget, this.topSliverWidgetHeight, this.appBar, @@ -139,14 +141,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { int _perRow = 4; double _scaleFactor = 3.0; double _baseScaleFactor = 3.0; - int? _scaleRestoreAssetIndex; + int? _restoreAssetIndex; @override void initState() { super.initState(); _scrollController = ScrollController( initialScrollOffset: widget.initialScrollOffset ?? 0.0, - onAttach: _restoreScalePosition, + onAttach: _restoreAssetPosition, ); _eventSubscription = EventStream.shared.listen(_onEvent); @@ -179,14 +181,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { EventStream.shared.emit(MultiSelectToggleEvent(isEnabled)); } - void _restoreScalePosition(_) { - if (_scaleRestoreAssetIndex == null) return; + void _restoreAssetPosition(_) { + if (_restoreAssetIndex == null) return; final asyncSegments = ref.read(timelineSegmentProvider); asyncSegments.whenData((segments) { - final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _scaleRestoreAssetIndex!); + final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _restoreAssetIndex!); if (targetSegment != null) { - final assetIndexInSegment = _scaleRestoreAssetIndex! - targetSegment.firstAssetIndex; + final assetIndexInSegment = _restoreAssetIndex! - targetSegment.firstAssetIndex; final newColumnCount = ref.read(timelineArgsProvider).columnCount; final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor(); final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment; @@ -198,7 +200,25 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { }); } }); - _scaleRestoreAssetIndex = null; + _restoreAssetIndex = null; + } + + int? _getCurrentAssetIndex(List segments) { + final currentOffset = _scrollController.offset.clamp(0.0, _scrollController.position.maxScrollExtent); + final segment = segments.findByOffset(currentOffset) ?? segments.lastOrNull; + int? targetAssetIndex; + if (segment != null) { + final rowIndex = segment.getMinChildIndexForScrollOffset(currentOffset); + if (rowIndex > segment.firstIndex) { + final rowIndexInSegment = rowIndex - (segment.firstIndex + 1); + final assetsPerRow = ref.read(timelineArgsProvider).columnCount; + final assetIndexInSegment = rowIndexInSegment * assetsPerRow; + targetAssetIndex = segment.firstAssetIndex + assetIndexInSegment; + } else { + targetAssetIndex = segment.firstAssetIndex; + } + } + return targetAssetIndex; } @override @@ -387,74 +407,66 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { return PrimaryScrollController( controller: _scrollController, - 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(); - - if (newPerRow != _perRow) { - final currentOffset = _scrollController.offset.clamp( - 0.0, - _scrollController.position.maxScrollExtent, - ); - final segment = segments.findByOffset(currentOffset) ?? segments.lastOrNull; - int? targetAssetIndex; - if (segment != null) { - final rowIndex = segment.getMinChildIndexForScrollOffset(currentOffset); - if (rowIndex > segment.firstIndex) { - final rowIndexInSegment = rowIndex - (segment.firstIndex + 1); - final assetsPerRow = ref.read(timelineArgsProvider).columnCount; - final assetIndexInSegment = rowIndexInSegment * assetsPerRow; - targetAssetIndex = segment.firstAssetIndex + assetIndexInSegment; - } else { - targetAssetIndex = segment.firstAssetIndex; - } - } - - setState(() { - _scaleFactor = newScaleFactor; - _perRow = newPerRow; - _scaleRestoreAssetIndex = targetAssetIndex; - }); - - ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow); - } - }; - }, - ), + child: NotificationListener( + onNotification: (notification) { + final currentIndex = _getCurrentAssetIndex(segments); + if (currentIndex != null && mounted) { + _restoreAssetIndex = currentIndex; + } + return false; }, - 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: 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(); + final targetAssetIndex = _getCurrentAssetIndex(segments); + + if (newPerRow != _perRow) { + setState(() { + _scaleFactor = newScaleFactor; + _perRow = newPerRow; + _restoreAssetIndex = targetAssetIndex; + }); + + ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow); + } + }; + }, + ), }, - child: Stack( - children: [ - timeline, - if (!isSelectionMode && isMultiSelectEnabled) ...[ - 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 (!isSelectionMode && isMultiSelectEnabled) ...[ + Positioned( + top: MediaQuery.paddingOf(context).top, + left: 25, + child: const SizedBox( + height: kToolbarHeight, + child: Center(child: _MultiSelectStatusButton()), + ), ), - ), - if (widget.bottomSheet != null) widget.bottomSheet!, + if (widget.bottomSheet != null) widget.bottomSheet!, + ], ], - ], + ), ), ), ),