From 4bd465e7525f750cdd98296775cf5f27f2fec67f Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 31 Jul 2025 21:02:28 -0500 Subject: [PATCH] feat: change grid size with gesture (#20455) --- .../widgets/timeline/timeline.widget.dart | 121 +++++++++++++----- 1 file changed, 86 insertions(+), 35 deletions(-) diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index d946872781..dcf2c74ed5 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -3,6 +3,7 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -88,10 +89,23 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { final _scrollController = ScrollController(); StreamSubscription? _eventSubscription; + int _perRow = 4; + double _scaleFactor = 3.0; + double _baseScaleFactor = 3.0; + @override void initState() { super.initState(); _eventSubscription = EventStream.shared.listen(_onEvent); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final currentTilesPerRow = ref.read(settingsProvider).get(Setting.tilesPerRow); + setState(() { + _perRow = currentTilesPerRow; + _scaleFactor = 7.0 - _perRow; + _baseScaleFactor = _scaleFactor; + }); + }); } void _onEvent(Event event) { @@ -177,43 +191,72 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { return PrimaryScrollController( controller: _scrollController, - child: Stack( - children: [ - Scrubber( - layoutSegments: segments, - timelineHeight: maxHeight, - topPadding: topPadding, - bottomPadding: bottomPadding, - monthSegmentSnappingOffset: widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight, - child: CustomScrollView( - primary: true, - cacheExtent: maxHeight * 2, - slivers: [ - if (isSelectionMode) const SelectionSliverAppBar() else if (widget.appBar != null) widget.appBar!, - if (widget.topSliverWidget != null) widget.topSliverWidget!, - _SliverSegmentedList( - segments: segments, - delegate: SliverChildBuilderDelegate( - (ctx, index) { - if (index >= childCount) return null; - final segment = segments.findByIndex(index); - return segment?.builder(ctx, index) ?? const SizedBox.shrink(); - }, - childCount: childCount, - addAutomaticKeepAlives: false, - // We add repaint boundary around tiles, so skip the auto boundaries - addRepaintBoundaries: false, - ), - ), - const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)), - ], - ), + 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) { + setState(() { + _scaleFactor = newScaleFactor; + _perRow = newPerRow; + }); + + ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow); + } + }; + }, ), - if (!isSelectionMode && isMultiSelectEnabled) ...[ - const Positioned(top: 60, left: 25, child: _MultiSelectStatusButton()), - if (widget.bottomSheet != null) widget.bottomSheet!, + }, + child: Stack( + children: [ + Scrubber( + layoutSegments: segments, + timelineHeight: maxHeight, + topPadding: topPadding, + bottomPadding: bottomPadding, + monthSegmentSnappingOffset: widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight, + child: CustomScrollView( + primary: true, + cacheExtent: maxHeight * 2, + slivers: [ + if (isSelectionMode) + const SelectionSliverAppBar() + else if (widget.appBar != null) + widget.appBar!, + if (widget.topSliverWidget != null) widget.topSliverWidget!, + _SliverSegmentedList( + segments: segments, + delegate: SliverChildBuilderDelegate( + (ctx, index) { + if (index >= childCount) return null; + final segment = segments.findByIndex(index); + return segment?.builder(ctx, index) ?? const SizedBox.shrink(); + }, + childCount: childCount, + addAutomaticKeepAlives: false, + // We add repaint boundary around tiles, so skip the auto boundaries + addRepaintBoundaries: false, + ), + ), + const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)), + ], + ), + ), + if (!isSelectionMode && isMultiSelectEnabled) ...[ + const Positioned(top: 60, left: 25, child: _MultiSelectStatusButton()), + if (widget.bottomSheet != null) widget.bottomSheet!, + ], ], - ], + ), ), ); }, @@ -443,3 +486,11 @@ class _MultiSelectStatusButton extends ConsumerWidget { ); } } + +/// accepts a gesture even though it should reject it (because child won) +class CustomScaleGestureRecognizer extends ScaleGestureRecognizer { + @override + void rejectGesture(int pointer) { + acceptGesture(pointer); + } +}