fix: show sync controls as compact top bar

close #548
This commit is contained in:
edde746
2026-02-26 03:41:19 +01:00
parent 3418241c04
commit 98776b40f5
6 changed files with 365 additions and 57 deletions
+39 -11
View File
@@ -48,14 +48,18 @@ class OverlaySheetController {
/// Whether a sheet is currently showing (including while animating closed).
bool get isOpen => _state._isOpen;
/// Show a bottom sheet with [builder] content. Returns a Future that completes
/// Show a sheet with [builder] content. Returns a Future that completes
/// when the sheet is closed (with an optional result).
///
/// [alignment] controls where the sheet appears. Defaults to
/// [Alignment.bottomCenter]. Use [Alignment.topCenter] to anchor at the top.
Future<T?> show<T>({
required WidgetBuilder builder,
BoxConstraints? constraints,
Color? backgroundColor,
bool barrierDismissible = true,
FocusNode? initialFocusNode,
Alignment alignment = Alignment.bottomCenter,
}) {
return _state._show<T>(
builder: builder,
@@ -63,6 +67,7 @@ class OverlaySheetController {
backgroundColor: backgroundColor,
barrierDismissible: barrierDismissible,
initialFocusNode: initialFocusNode,
alignment: alignment,
);
}
@@ -98,6 +103,7 @@ class OverlaySheetController {
bool barrierDismissible = true,
bool isScrollControlled = false,
FocusNode? initialFocusNode,
Alignment alignment = Alignment.bottomCenter,
}) {
final controller = maybeOf(context);
if (controller != null) {
@@ -107,6 +113,7 @@ class OverlaySheetController {
backgroundColor: backgroundColor,
barrierDismissible: barrierDismissible,
initialFocusNode: initialFocusNode,
alignment: alignment,
);
}
return showModalBottomSheet<T>(
@@ -176,7 +183,7 @@ class OverlaySheetHost extends StatefulWidget {
class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
late final Animation<Offset> _slideAnimation;
late final CurvedAnimation _slideCurve;
late final Animation<double> _barrierAnimation;
late final OverlaySheetController _controller;
@@ -188,6 +195,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
bool _barrierDismissible = true;
BoxConstraints? _constraints;
Color? _explicitBackgroundColor;
Alignment _alignment = Alignment.bottomCenter;
// Drag-to-dismiss state
double _dragOffset = 0;
@@ -200,8 +208,10 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
_animationController = AnimationController(duration: const Duration(milliseconds: 250), vsync: this);
_slideAnimation = Tween<Offset>(begin: const Offset(0, 1), end: Offset.zero).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic, reverseCurve: Curves.easeInCubic),
_slideCurve = CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
);
_barrierAnimation = Tween<double>(
@@ -218,6 +228,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
}
}
_sheetFocusScopeNode.dispose();
_slideCurve.dispose();
_animationController.dispose();
super.dispose();
}
@@ -228,6 +239,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
Color? backgroundColor,
bool barrierDismissible = true,
FocusNode? initialFocusNode,
Alignment alignment = Alignment.bottomCenter,
}) {
// If already open, close first (instant)
if (_isOpen) {
@@ -250,6 +262,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
_barrierDismissible = barrierDismissible;
_constraints = constraints;
_explicitBackgroundColor = backgroundColor;
_alignment = alignment;
_dragOffset = 0;
_isDragging = false;
});
@@ -448,29 +461,44 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
Widget _buildSheet(BuildContext context) {
final size = MediaQuery.of(context).size;
final isDesktop = size.width > 600;
final isTop = _alignment.y < 0;
final effectiveConstraints =
_constraints ??
BoxConstraints(maxWidth: isDesktop ? 700 : double.infinity, maxHeight: isDesktop ? 400 : size.height * 0.75);
// Slide direction depends on alignment: bottom sheets slide up, top sheets slide down.
final slideBegin = isTop ? const Offset(0, -1) : const Offset(0, 1);
final borderRadius = isTop
? const BorderRadius.vertical(bottom: Radius.circular(16))
: const BorderRadius.vertical(top: Radius.circular(16));
Widget sheet = FocusScope(
node: _sheetFocusScopeNode,
child: Focus(
canRequestFocus: false,
skipTraversal: true,
onKeyEvent: _handleKeyEvent,
child: SlideTransition(
position: _slideAnimation,
child: AnimatedBuilder(
animation: _slideCurve,
builder: (context, child) {
final slideOffset = Offset.lerp(slideBegin, Offset.zero, _slideCurve.value)!;
return FractionalTranslation(
translation: slideOffset,
child: child,
);
},
child: Align(
alignment: Alignment.bottomCenter,
alignment: _alignment,
child: Transform.translate(
offset: Offset(0, _dragOffset.clamp(0, double.infinity)),
child: Material(
color: _explicitBackgroundColor ?? Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
borderRadius: borderRadius,
clipBehavior: Clip.antiAlias,
child: SafeArea(
top: false,
top: !isTop,
bottom: isTop,
child: ConstrainedBox(
constraints: effectiveConstraints,
child: _pageStack.isNotEmpty ? _pageStack.last.builder(context) : const SizedBox.shrink(),
@@ -483,8 +511,8 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
),
);
// Swipe-down-to-dismiss (skip on TV where there's no touchscreen)
if (!PlatformDetector.isTV()) {
// Swipe-down-to-dismiss (skip on TV and for top-aligned sheets)
if (!PlatformDetector.isTV() && !isTop) {
sheet = GestureDetector(
onVerticalDragStart: (_) {
_isDragging = true;