diff --git a/lib/utils/snackbar_helper.dart b/lib/utils/snackbar_helper.dart index a77efc16..8f877953 100644 --- a/lib/utils/snackbar_helper.dart +++ b/lib/utils/snackbar_helper.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'layout_constants.dart'; +import 'platform_detector.dart'; /// Global key for the root ScaffoldMessenger, allowing snackbars to survive navigation. final rootScaffoldMessengerKey = GlobalKey(); @@ -12,19 +13,82 @@ final mainScaffoldMessengerKey = GlobalKey(); /// Types of snackbars available in the app enum SnackBarType { info, success, error } +const double _kDesktopSnackBarMaxWidth = 480.0; +const double _kDesktopSnackBarHorizontalInset = 16.0; +const EdgeInsets _kDismissibleSnackBarPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 14); + /// Utility functions for showing snackbars throughout the application -void showSnackBar(BuildContext context, String message, {SnackBarType type = SnackBarType.info, Duration? duration}) { +SnackBar _buildSnackBar( + BuildContext context, + ScaffoldMessengerState messenger, { + required Widget content, + required Color? backgroundColor, + required Duration duration, + bool? dismissible, +}) { + final isDesktop = PlatformDetector.isDesktopOS(); + final tapToDismiss = dismissible ?? isDesktop; + final body = tapToDismiss + ? MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => messenger.hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss), + child: Padding(padding: _kDismissibleSnackBarPadding, child: content), + ), + ) + : content; + + return SnackBar( + content: body, + backgroundColor: backgroundColor, + duration: duration, + behavior: isDesktop ? SnackBarBehavior.floating : null, + width: isDesktop ? _desktopSnackBarWidth(context) : null, + padding: tapToDismiss ? EdgeInsets.zero : null, + ); +} + +double _desktopSnackBarWidth(BuildContext context) { + final windowWidth = MediaQuery.maybeSizeOf(context)?.width; + if (windowWidth == null) return _kDesktopSnackBarMaxWidth; + + final insetPadding = SnackBarTheme.of(context).insetPadding; + final horizontalInset = insetPadding == null + ? _kDesktopSnackBarHorizontalInset * 2 + : insetPadding.left + insetPadding.right; + final availableWidth = windowWidth - horizontalInset; + final width = availableWidth > 0 ? availableWidth : windowWidth; + return width < _kDesktopSnackBarMaxWidth ? width : _kDesktopSnackBarMaxWidth; +} + +(Color?, Duration) _snackBarStyle(SnackBarType type) => switch (type) { + SnackBarType.info => (null, AppDurations.snackBarDefault), + SnackBarType.success => (Colors.green, AppDurations.snackBarDefault), + SnackBarType.error => (Colors.red, AppDurations.snackBarLong), +}; + +void showSnackBar( + BuildContext context, + String message, { + SnackBarType type = SnackBarType.info, + Duration? duration, + bool? dismissible, +}) { if (!context.mounted) return; - final (backgroundColor, defaultDuration) = switch (type) { - SnackBarType.info => (null, AppDurations.snackBarDefault), - SnackBarType.success => (Colors.green, AppDurations.snackBarDefault), - SnackBarType.error => (Colors.red, AppDurations.snackBarLong), - }; - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: backgroundColor, duration: duration ?? defaultDuration), + final (backgroundColor, defaultDuration) = _snackBarStyle(type); + final messenger = ScaffoldMessenger.of(context); + messenger.showSnackBar( + _buildSnackBar( + context, + messenger, + content: Text(message), + backgroundColor: backgroundColor, + duration: duration ?? defaultDuration, + dismissible: dismissible, + ), ); } @@ -38,8 +102,16 @@ void showErrorSnackBar(BuildContext context, String message) { /// Shows an error snackbar using the root ScaffoldMessenger (survives navigation). void showGlobalErrorSnackBar(String message) { - rootScaffoldMessengerKey.currentState?.showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.red, duration: AppDurations.snackBarLong), + final messenger = rootScaffoldMessengerKey.currentState; + if (messenger == null) return; + messenger.showSnackBar( + _buildSnackBar( + messenger.context, + messenger, + content: Text(message), + backgroundColor: Colors.red, + duration: AppDurations.snackBarLong, + ), ); } @@ -48,9 +120,12 @@ void showGlobalErrorSnackBar(String message) { /// messenger when the main screen is not mounted. void showMainSnackBar(String message, {Duration duration = AppDurations.snackBarDefault}) { final messenger = mainScaffoldMessengerKey.currentState ?? rootScaffoldMessengerKey.currentState; + if (messenger == null) return; messenger - ?..removeCurrentSnackBar() - ..showSnackBar(SnackBar(content: Text(message), duration: duration)); + ..removeCurrentSnackBar() + ..showSnackBar( + _buildSnackBar(messenger.context, messenger, content: Text(message), backgroundColor: null, duration: duration), + ); } void showSuccessSnackBar(BuildContext context, String message) {