feat: generic control bottom app bar (#19524)

* feat: sliver appbar

* feat: snapping segment

* Date label font size

* lint

* fix: scrollController reinitialize multiple times

* feat: tab navigation

* chore: refactor to private widget

* feat: new control bottom app bar

* bad merge

* feat: sliver control bottom app bar
This commit is contained in:
Alex
2025-06-25 11:08:02 -05:00
committed by GitHub
parent afb444c92c
commit b001ba44f5
7 changed files with 255 additions and 20 deletions
@@ -13,7 +13,7 @@ import 'package:intl/intl.dart' hide TextDirection;
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
/// for quick navigation of the BoxScrollView.
class Scrubber extends StatefulWidget {
class Scrubber extends ConsumerStatefulWidget {
/// The view that will be scrolled with the scroll thumb
final CustomScrollView child;
@@ -37,7 +37,7 @@ class Scrubber extends StatefulWidget {
}) : assert(child.scrollDirection == Axis.vertical);
@override
State createState() => ScrubberState();
ConsumerState createState() => ScrubberState();
}
List<_Segment> _buildSegments({
@@ -82,7 +82,8 @@ List<_Segment> _buildSegments({
return segments;
}
class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
class ScrubberState extends ConsumerState<Scrubber>
with TickerProviderStateMixin {
double _thumbTopOffset = 0.0;
bool _isDragging = false;
List<_Segment> _segments = [];
@@ -175,6 +176,13 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
return false;
}
if (notification is ScrollStartNotification ||
notification is ScrollUpdateNotification) {
ref.read(timelineStateProvider.notifier).setScrolling(true);
} else if (notification is ScrollEndNotification) {
ref.read(timelineStateProvider.notifier).setScrolling(false);
}
setState(() {
if (notification is ScrollUpdateNotification) {
_thumbTopOffset = _currentOffset;
@@ -191,7 +199,7 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
return false;
}
void _onDragStart(WidgetRef ref) {
void _onDragStart(DragStartDetails _) {
ref.read(timelineStateProvider.notifier).setScrubbing(true);
setState(() {
_isDragging = true;
@@ -293,10 +301,12 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
_scrollController.jumpTo(centeredOffset.clamp(0.0, maxScrollExtent));
}
void _onDragEnd(WidgetRef ref) {
void _onDragEnd(DragEndDetails _) {
ref.read(timelineStateProvider.notifier).setScrubbing(false);
_labelAnimationController.reverse();
_isDragging = false;
setState(() {
_isDragging = false;
});
_resetThumbTimer();
}
@@ -342,13 +352,10 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
top: _thumbTopOffset + widget.topPadding,
end: 0,
child: RepaintBoundary(
child: Consumer(
builder: (_, ref, child) => GestureDetector(
onVerticalDragStart: (_) => _onDragStart(ref),
onVerticalDragUpdate: _onDragUpdate,
onVerticalDragEnd: (_) => _onDragEnd(ref),
child: child,
),
child: GestureDetector(
onVerticalDragStart: _onDragStart,
onVerticalDragUpdate: _onDragUpdate,
onVerticalDragEnd: _onDragEnd,
child: _Scrubber(
thumbAnimation: _thumbAnimation,
labelAnimation: _labelAnimation,
@@ -40,19 +40,28 @@ class TimelineArgs {
class TimelineState {
final bool isScrubbing;
final bool isScrolling;
const TimelineState({this.isScrubbing = false});
const TimelineState({
this.isScrubbing = false,
this.isScrolling = false,
});
bool get isInteracting => isScrubbing || isScrolling;
@override
bool operator ==(covariant TimelineState other) {
return isScrubbing == other.isScrubbing;
return isScrubbing == other.isScrubbing && isScrolling == other.isScrolling;
}
@override
int get hashCode => isScrubbing.hashCode;
int get hashCode => isScrubbing.hashCode ^ isScrolling.hashCode;
TimelineState copyWith({bool? isScrubbing}) {
return TimelineState(isScrubbing: isScrubbing ?? this.isScrubbing);
TimelineState copyWith({bool? isScrubbing, bool? isScrolling}) {
return TimelineState(
isScrubbing: isScrubbing ?? this.isScrubbing,
isScrolling: isScrolling ?? this.isScrolling,
);
}
}
@@ -63,8 +72,15 @@ class TimelineStateNotifier extends Notifier<TimelineState> {
state = state.copyWith(isScrubbing: isScrubbing);
}
void setScrolling(bool isScrolling) {
state = state.copyWith(isScrolling: isScrolling);
}
@override
TimelineState build() => const TimelineState(isScrubbing: false);
TimelineState build() => const TimelineState(
isScrubbing: false,
isScrolling: false,
);
}
// This provider watches the buckets from the timeline service & args and serves the segments.
@@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
@@ -120,12 +121,14 @@ class _SliverTimelineState extends State<_SliverTimeline> {
],
),
),
if (isMultiSelectEnabled)
if (isMultiSelectEnabled) ...[
const Positioned(
top: 60,
left: 25,
child: _MultiSelectStatusButton(),
),
const HomeBottomAppBar(),
],
],
),
);