init cc
This commit is contained in:
621
lib/core/widgets/app_snackbar.dart
Normal file
621
lib/core/widgets/app_snackbar.dart
Normal file
@@ -0,0 +1,621 @@
|
||||
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<SnackBar, SnackBarClosedReason> 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<SnackBar, SnackBarClosedReason> 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<SnackBar, SnackBarClosedReason> 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<SnackBar, SnackBarClosedReason> 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<SnackBar, SnackBarClosedReason> 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<SnackBar, SnackBarClosedReason> 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<Color>(
|
||||
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<SnackBar, SnackBarClosedReason> 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<SnackBar, SnackBarClosedReason> 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<SnackBar, SnackBarClosedReason> 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<SnackBar, SnackBarClosedReason> 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<SnackBar, SnackBarClosedReason> _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<SnackBar, SnackBarClosedReason> 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<Widget>? 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<SnackBar, SnackBarClosedReason> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user