import 'package:flutter/material.dart'; import '../theme/app_spacing.dart'; import '../theme/app_typography.dart'; /// Snackbar utilities for consistent notifications and user feedback /// /// Provides easy-to-use static methods for showing different types of /// snackbars with Material 3 styling and customization options. class AppSnackbar { // Prevent instantiation AppSnackbar._(); /// Show a basic snackbar static ScaffoldFeatureController show({ required BuildContext context, required String message, Duration duration = const Duration(seconds: 4), SnackBarAction? action, Color? backgroundColor, Color? textColor, double? elevation, EdgeInsets? margin, ShapeBorder? shape, SnackBarBehavior behavior = SnackBarBehavior.floating, bool showCloseIcon = false, VoidCallback? onVisible, }) { final theme = Theme.of(context); final snackBarTheme = theme.snackBarTheme; final snackBar = SnackBar( content: Text( message, style: AppTypography.bodyMedium.copyWith( color: textColor ?? snackBarTheme.contentTextStyle?.color, ), ), duration: duration, action: action, backgroundColor: backgroundColor ?? snackBarTheme.backgroundColor, elevation: elevation ?? snackBarTheme.elevation, margin: margin ?? _getDefaultMargin(context), shape: shape ?? snackBarTheme.shape, behavior: behavior, showCloseIcon: showCloseIcon, onVisible: onVisible, ); return ScaffoldMessenger.of(context).showSnackBar(snackBar); } /// Show a success snackbar static ScaffoldFeatureController showSuccess({ required BuildContext context, required String message, Duration duration = const Duration(seconds: 3), SnackBarAction? action, bool showCloseIcon = false, VoidCallback? onVisible, }) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final successColor = _getSuccessColor(theme); return _showWithIcon( context: context, message: message, icon: Icons.check_circle_outline, backgroundColor: successColor, textColor: _getOnColor(successColor), iconColor: _getOnColor(successColor), duration: duration, action: action, showCloseIcon: showCloseIcon, onVisible: onVisible, ); } /// Show an error snackbar static ScaffoldFeatureController showError({ required BuildContext context, required String message, Duration duration = const Duration(seconds: 5), SnackBarAction? action, bool showCloseIcon = true, VoidCallback? onVisible, }) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return _showWithIcon( context: context, message: message, icon: Icons.error_outline, backgroundColor: colorScheme.error, textColor: colorScheme.onError, iconColor: colorScheme.onError, duration: duration, action: action, showCloseIcon: showCloseIcon, onVisible: onVisible, ); } /// Show a warning snackbar static ScaffoldFeatureController showWarning({ required BuildContext context, required String message, Duration duration = const Duration(seconds: 4), SnackBarAction? action, bool showCloseIcon = false, VoidCallback? onVisible, }) { final theme = Theme.of(context); final warningColor = _getWarningColor(theme); return _showWithIcon( context: context, message: message, icon: Icons.warning_outlined, backgroundColor: warningColor, textColor: _getOnColor(warningColor), iconColor: _getOnColor(warningColor), duration: duration, action: action, showCloseIcon: showCloseIcon, onVisible: onVisible, ); } /// Show an info snackbar static ScaffoldFeatureController showInfo({ required BuildContext context, required String message, Duration duration = const Duration(seconds: 4), SnackBarAction? action, bool showCloseIcon = false, VoidCallback? onVisible, }) { final theme = Theme.of(context); final infoColor = _getInfoColor(theme); return _showWithIcon( context: context, message: message, icon: Icons.info_outline, backgroundColor: infoColor, textColor: _getOnColor(infoColor), iconColor: _getOnColor(infoColor), duration: duration, action: action, showCloseIcon: showCloseIcon, onVisible: onVisible, ); } /// Show a loading snackbar static ScaffoldFeatureController showLoading({ required BuildContext context, String message = 'Loading...', Duration duration = const Duration(seconds: 30), bool showCloseIcon = false, VoidCallback? onVisible, }) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final snackBar = SnackBar( content: Row( children: [ SizedBox( width: AppSpacing.iconSM, height: AppSpacing.iconSM, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( colorScheme.onSurfaceVariant, ), ), ), AppSpacing.horizontalSpaceMD, Expanded( child: Text( message, style: AppTypography.bodyMedium.copyWith( color: colorScheme.onSurfaceVariant, ), ), ), ], ), duration: duration, backgroundColor: colorScheme.surfaceContainerHighest, margin: _getDefaultMargin(context), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: AppSpacing.radiusSM, ), showCloseIcon: showCloseIcon, onVisible: onVisible, ); return ScaffoldMessenger.of(context).showSnackBar(snackBar); } /// Show an action snackbar with custom button static ScaffoldFeatureController showAction({ required BuildContext context, required String message, required String actionLabel, required VoidCallback onActionPressed, Duration duration = const Duration(seconds: 6), Color? backgroundColor, Color? textColor, Color? actionColor, bool showCloseIcon = false, VoidCallback? onVisible, }) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return show( context: context, message: message, duration: duration, backgroundColor: backgroundColor, textColor: textColor, showCloseIcon: showCloseIcon, onVisible: onVisible, action: SnackBarAction( label: actionLabel, onPressed: onActionPressed, textColor: actionColor ?? colorScheme.inversePrimary, ), ); } /// Show an undo snackbar static ScaffoldFeatureController showUndo({ required BuildContext context, required String message, required VoidCallback onUndo, String undoLabel = 'Undo', Duration duration = const Duration(seconds: 5), VoidCallback? onVisible, }) { return showAction( context: context, message: message, actionLabel: undoLabel, onActionPressed: onUndo, duration: duration, onVisible: onVisible, ); } /// Show a retry snackbar static ScaffoldFeatureController showRetry({ required BuildContext context, String message = 'Something went wrong', required VoidCallback onRetry, String retryLabel = 'Retry', Duration duration = const Duration(seconds: 6), VoidCallback? onVisible, }) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return _showWithIcon( context: context, message: message, icon: Icons.refresh, backgroundColor: colorScheme.errorContainer, textColor: colorScheme.onErrorContainer, iconColor: colorScheme.onErrorContainer, duration: duration, action: SnackBarAction( label: retryLabel, onPressed: onRetry, textColor: colorScheme.error, ), onVisible: onVisible, ); } /// Show a custom snackbar with icon static ScaffoldFeatureController showCustom({ required BuildContext context, required String message, IconData? icon, Color? iconColor, Color? backgroundColor, Color? textColor, Duration duration = const Duration(seconds: 4), SnackBarAction? action, bool showCloseIcon = false, VoidCallback? onVisible, }) { if (icon != null) { return _showWithIcon( context: context, message: message, icon: icon, backgroundColor: backgroundColor, textColor: textColor, iconColor: iconColor, duration: duration, action: action, showCloseIcon: showCloseIcon, onVisible: onVisible, ); } return show( context: context, message: message, backgroundColor: backgroundColor, textColor: textColor, duration: duration, action: action, showCloseIcon: showCloseIcon, onVisible: onVisible, ); } /// Hide current snackbar static void hide(BuildContext context) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); } /// Remove all snackbars static void removeAll(BuildContext context) { ScaffoldMessenger.of(context).clearSnackBars(); } // Helper methods /// Show snackbar with icon static ScaffoldFeatureController _showWithIcon({ required BuildContext context, required String message, required IconData icon, Color? backgroundColor, Color? textColor, Color? iconColor, Duration duration = const Duration(seconds: 4), SnackBarAction? action, bool showCloseIcon = false, VoidCallback? onVisible, }) { final theme = Theme.of(context); final snackBarTheme = theme.snackBarTheme; final snackBar = SnackBar( content: Row( children: [ Icon( icon, size: AppSpacing.iconSM, color: iconColor ?? textColor, ), AppSpacing.horizontalSpaceMD, Expanded( child: Text( message, style: AppTypography.bodyMedium.copyWith( color: textColor ?? snackBarTheme.contentTextStyle?.color, ), ), ), ], ), duration: duration, action: action, backgroundColor: backgroundColor ?? snackBarTheme.backgroundColor, elevation: snackBarTheme.elevation, margin: _getDefaultMargin(context), shape: snackBarTheme.shape, behavior: SnackBarBehavior.floating, showCloseIcon: showCloseIcon, onVisible: onVisible, ); return ScaffoldMessenger.of(context).showSnackBar(snackBar); } /// Get default margin for floating snackbars static EdgeInsets _getDefaultMargin(BuildContext context) { return const EdgeInsets.all(AppSpacing.md); } /// Get success color from theme extension or fallback static Color _getSuccessColor(ThemeData theme) { return const Color(0xFF4CAF50); // Material Green 500 } /// Get warning color from theme extension or fallback static Color _getWarningColor(ThemeData theme) { return const Color(0xFFFF9800); // Material Orange 500 } /// Get info color from theme extension or fallback static Color _getInfoColor(ThemeData theme) { return theme.colorScheme.primary; } /// Get appropriate "on" color for given background color static Color _getOnColor(Color backgroundColor) { // Simple calculation based on luminance return backgroundColor.computeLuminance() > 0.5 ? Colors.black87 : Colors.white; } } /// Custom snackbar widget for more complex layouts class AppCustomSnackbar extends StatelessWidget { /// Creates a custom snackbar widget const AppCustomSnackbar({ super.key, required this.child, this.backgroundColor, this.elevation, this.margin, this.shape, this.behavior = SnackBarBehavior.floating, this.width, this.padding, }); /// The content to display in the snackbar final Widget child; /// Background color override final Color? backgroundColor; /// Elevation override final double? elevation; /// Margin around the snackbar final EdgeInsets? margin; /// Shape override final ShapeBorder? shape; /// Snackbar behavior final SnackBarBehavior behavior; /// Fixed width override final double? width; /// Content padding final EdgeInsets? padding; @override Widget build(BuildContext context) { final theme = Theme.of(context); final snackBarTheme = theme.snackBarTheme; Widget content = Container( width: width, padding: padding ?? const EdgeInsets.all(AppSpacing.md), child: child, ); return SnackBar( content: content, backgroundColor: backgroundColor ?? snackBarTheme.backgroundColor, elevation: elevation ?? snackBarTheme.elevation, margin: margin ?? const EdgeInsets.all(AppSpacing.md), shape: shape ?? snackBarTheme.shape, behavior: behavior, padding: EdgeInsets.zero, // Remove default padding since we handle it ); } /// Show this custom snackbar ScaffoldFeatureController show({ required BuildContext context, Duration duration = const Duration(seconds: 4), }) { return ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: child, backgroundColor: backgroundColor, elevation: elevation, margin: margin ?? const EdgeInsets.all(AppSpacing.md), shape: shape, behavior: behavior, duration: duration, padding: EdgeInsets.zero, ), ); } } /// Snackbar with rich content (title, subtitle, actions) class AppRichSnackbar extends StatelessWidget { /// Creates a rich snackbar with title, subtitle, and actions const AppRichSnackbar({ super.key, required this.title, this.subtitle, this.icon, this.actions, this.backgroundColor, this.onTap, }); /// Main title text final String title; /// Optional subtitle text final String? subtitle; /// Optional leading icon final IconData? icon; /// Optional action widgets final List? actions; /// Background color override final Color? backgroundColor; /// Tap callback for the entire snackbar final VoidCallback? onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; Widget content = Row( children: [ if (icon != null) ...[ Icon( icon, size: AppSpacing.iconMD, color: backgroundColor != null ? _getOnColor(backgroundColor!) : colorScheme.onSurfaceVariant, ), AppSpacing.horizontalSpaceMD, ], Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: AppTypography.titleSmall.copyWith( color: backgroundColor != null ? _getOnColor(backgroundColor!) : colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (subtitle != null) ...[ AppSpacing.verticalSpaceXS, Text( subtitle!, style: AppTypography.bodySmall.copyWith( color: backgroundColor != null ? _getOnColor(backgroundColor!).withOpacity(0.8) : colorScheme.onSurfaceVariant.withOpacity(0.8), ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ], ), ), if (actions != null && actions!.isNotEmpty) ...[ AppSpacing.horizontalSpaceSM, Row(children: actions!), ], ], ); if (onTap != null) { content = InkWell( onTap: onTap, borderRadius: AppSpacing.radiusSM, child: content, ); } return AppCustomSnackbar( backgroundColor: backgroundColor, child: content, ); } /// Show this rich snackbar ScaffoldFeatureController show({ required BuildContext context, Duration duration = const Duration(seconds: 5), }) { return ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: this, duration: duration, backgroundColor: Colors.transparent, elevation: 0, behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(AppSpacing.md), padding: EdgeInsets.zero, ), ); } /// Get appropriate "on" color for given background color static Color _getOnColor(Color backgroundColor) { return backgroundColor.computeLuminance() > 0.5 ? Colors.black87 : Colors.white; } }