Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixUI/hesabix_ui/lib/utils/snackbar_helper.dart

369 lines
14 KiB
Dart
Executable file

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'dart:async';
import '../main.dart' show navigatorKey;
import '../theme/semantic_color_resolver.dart';
/// Helper utility for showing SnackBar messages that appear above dialogs.
///
/// When multiple dialogs and bottom sheets are stacked, regular ScaffoldMessenger.of(context)
/// shows SnackBar below them. This helper finds the root ScaffoldMessenger
/// to ensure SnackBar appears above all dialogs and bottom sheets.
class SnackBarHelper {
/// Determines the appropriate text color based on background brightness
static Color _getTextColorForBackground(Color backgroundColor) {
final brightness = ThemeData.estimateBrightnessForColor(backgroundColor);
return brightness == Brightness.dark ? Colors.white : Colors.black87;
}
/// Gets the default background color for snackbars based on theme
static Color _getDefaultBackgroundColor(ThemeData theme, bool isError, BuildContext context) {
if (isError) {
return SemanticColorResolver.negative(context);
}
// Use a color that works well in both light and dark themes
final brightness = theme.brightness;
return brightness == Brightness.dark
? theme.colorScheme.surfaceContainerHighest
: theme.colorScheme.inverseSurface;
}
/// Shows a SnackBar using the root ScaffoldMessenger to ensure it appears
/// above all dialogs.
///
/// [context] - The current BuildContext (can be from a dialog)
/// [message] - The message to display
/// [backgroundColor] - Optional background color (defaults to theme error color for errors)
/// [isError] - If true, uses error styling
/// [duration] - How long to show the SnackBar
/// [action] - Optional action button
static void show(
BuildContext context, {
required String message,
Color? backgroundColor,
bool isError = false,
Duration? duration,
SnackBarAction? action,
}) {
debugPrint('[SnackBarHelper.show] Called with message: "$message"');
debugPrint('[SnackBarHelper.show] Context type: ${context.runtimeType}');
debugPrint('[SnackBarHelper.show] Context mounted: ${context.mounted}');
if (!context.mounted) {
debugPrint('[SnackBarHelper.show] ✗ Context not mounted, returning');
return;
}
// Try to use OverlayEntry method first (most reliable for stacked dialogs)
final rootContext = navigatorKey.currentContext;
if (rootContext != null) {
if (rootContext.mounted) {
debugPrint('[SnackBarHelper.show] Attempting to show via OverlayEntry in root context');
try {
_showViaOverlay(rootContext, message: message, backgroundColor: backgroundColor, isError: isError, duration: duration, action: action);
debugPrint('[SnackBarHelper.show] ✓ Successfully shown via OverlayEntry');
return;
} catch (e) {
debugPrint('[SnackBarHelper.show] ✗ OverlayEntry method failed: $e, falling back to ScaffoldMessenger');
}
} else {
debugPrint('[SnackBarHelper.show] ✗ Root context not mounted, falling back to ScaffoldMessenger');
}
} else {
debugPrint('[SnackBarHelper.show] ✗ Root context is null, falling back to ScaffoldMessenger');
}
// Fallback to ScaffoldMessenger method
debugPrint('[SnackBarHelper.show] Falling back to ScaffoldMessenger method...');
final rootMessenger = _findRootScaffoldMessenger(context);
debugPrint('[SnackBarHelper.show] Found ScaffoldMessenger: ${rootMessenger.runtimeType}');
final theme = Theme.of(context);
final effectiveBackgroundColor = backgroundColor ??
_getDefaultBackgroundColor(theme, isError, context);
final textColor = _getTextColorForBackground(effectiveBackgroundColor);
debugPrint('[SnackBarHelper.show] Showing SnackBar with message: "$message"');
rootMessenger.showSnackBar(
SnackBar(
content: Text(
message,
style: TextStyle(color: textColor),
),
backgroundColor: effectiveBackgroundColor,
duration: duration ?? const Duration(seconds: 4),
action: action,
),
);
debugPrint('[SnackBarHelper.show] ✓ SnackBar.showSnackBar called');
}
/// Shows SnackBar using OverlayEntry directly in root Overlay
/// This ensures it appears above all dialogs and bottom sheets
static void _showViaOverlay(
BuildContext rootContext, {
required String message,
Color? backgroundColor,
bool isError = false,
Duration? duration,
SnackBarAction? action,
}) {
// Get root Navigator's overlay which is above all dialogs
final rootNavigator = Navigator.of(rootContext, rootNavigator: true);
final overlay = rootNavigator.overlay;
if (overlay == null) {
throw Exception('Root overlay not found');
}
final theme = Theme.of(rootContext);
final effectiveBackgroundColor = backgroundColor ??
_getDefaultBackgroundColor(theme, isError, rootContext);
final textColor = _getTextColorForBackground(effectiveBackgroundColor);
final effectiveDuration = duration ?? const Duration(seconds: 4);
late OverlayEntry overlayEntry;
Timer? timer;
overlayEntry = OverlayEntry(
builder: (context) {
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: SafeArea(
child: Material(
color: Colors.transparent,
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Material(
elevation: 6,
borderRadius: BorderRadius.circular(4),
color: effectiveBackgroundColor,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Expanded(
child: Text(
message,
style: TextStyle(color: textColor),
),
),
if (action != null)
TextButton(
onPressed: () {
action.onPressed();
overlayEntry.remove();
timer?.cancel();
},
child: Text(
action.label,
style: TextStyle(color: textColor),
),
),
],
),
),
),
),
),
),
);
},
);
overlay.insert(overlayEntry);
// Auto-remove after duration
timer = Timer(effectiveDuration, () {
overlayEntry.remove();
});
}
/// Shows an error SnackBar
static void showError(
BuildContext context, {
required String message,
Duration? duration,
SnackBarAction? action,
}) {
show(
context,
message: message,
isError: true,
duration: duration,
action: action,
);
}
/// Shows a success SnackBar
static void showSuccess(
BuildContext context, {
required String message,
Duration? duration,
}) {
show(
context,
message: message,
backgroundColor: SemanticColorResolver.positive(context),
duration: duration,
);
}
/// Shows an info SnackBar
static void showInfo(
BuildContext context, {
required String message,
Duration? duration,
}) {
show(
context,
message: message,
backgroundColor: SemanticColorResolver.info(context),
duration: duration,
);
}
/// Shows a warning SnackBar
static void showWarning(
BuildContext context, {
required String message,
Duration? duration,
}) {
show(
context,
message: message,
backgroundColor: SemanticColorResolver.warning(context),
duration: duration,
);
}
/// Finds the root ScaffoldMessenger by using the global navigatorKey.
/// This ensures SnackBar appears above all dialogs and bottom sheets.
static ScaffoldMessengerState _findRootScaffoldMessenger(BuildContext context) {
debugPrint('[SnackBarHelper] Starting to find root ScaffoldMessenger');
debugPrint('[SnackBarHelper] Current context type: ${context.runtimeType}');
// Strategy 1: Use the global navigatorKey which points to the root Navigator
// This is the most reliable method when multiple dialogs/bottom sheets are stacked
final rootContext = navigatorKey.currentContext;
debugPrint('[SnackBarHelper] Strategy 1: navigatorKey.currentContext = ${rootContext != null ? rootContext.runtimeType : "null"}');
if (rootContext != null && rootContext.mounted) {
try {
final messenger = ScaffoldMessenger.maybeOf(rootContext);
if (messenger != null) {
debugPrint('[SnackBarHelper] ✓ Strategy 1 SUCCESS: Found ScaffoldMessenger via navigatorKey');
return messenger;
} else {
debugPrint('[SnackBarHelper] ✗ Strategy 1 FAILED: ScaffoldMessenger.maybeOf returned null');
}
} catch (e) {
debugPrint('[SnackBarHelper] ✗ Strategy 1 ERROR: $e');
}
} else {
debugPrint('[SnackBarHelper] ✗ Strategy 1 SKIPPED: rootContext is null or not mounted');
}
// Strategy 2: Get root navigator from current context and find ScaffoldMessenger
// This works when we're inside a dialog but navigatorKey is not available
try {
final rootNavigator = Navigator.of(context, rootNavigator: true);
final rootNavContext = rootNavigator.context;
debugPrint('[SnackBarHelper] Strategy 2: rootNavigator.context = ${rootNavContext.runtimeType}');
if (rootNavContext.mounted) {
final messenger = ScaffoldMessenger.maybeOf(rootNavContext);
if (messenger != null) {
debugPrint('[SnackBarHelper] ✓ Strategy 2 SUCCESS: Found ScaffoldMessenger via rootNavigator');
return messenger;
} else {
debugPrint('[SnackBarHelper] ✗ Strategy 2 FAILED: ScaffoldMessenger.maybeOf returned null');
}
} else {
debugPrint('[SnackBarHelper] ✗ Strategy 2 FAILED: rootNavContext is not mounted');
}
} catch (e) {
debugPrint('[SnackBarHelper] ✗ Strategy 2 ERROR: $e');
}
// Strategy 3: Traverse up the context tree to find ScaffoldMessenger
// This handles edge cases where we're deep in a dialog/bottom sheet stack
debugPrint('[SnackBarHelper] Strategy 3: Traversing up context tree...');
BuildContext? currentContext = context;
ScaffoldMessengerState? foundMessenger;
int maxDepth = 20; // Prevent infinite loops
int depth = 0;
while (currentContext != null && depth < maxDepth) {
depth++;
try {
if (currentContext.mounted) {
final messenger = ScaffoldMessenger.maybeOf(currentContext);
if (messenger != null) {
debugPrint('[SnackBarHelper] Strategy 3: Found ScaffoldMessenger at depth $depth, context type: ${currentContext.runtimeType}');
// Check if this context has a root Navigator (meaning it's at root level)
try {
final navigator = Navigator.maybeOf(currentContext, rootNavigator: true);
if (navigator != null) {
debugPrint('[SnackBarHelper] ✓ Strategy 3 SUCCESS: Found root ScaffoldMessenger at depth $depth');
foundMessenger = messenger;
break;
} else {
debugPrint('[SnackBarHelper] Strategy 3: ScaffoldMessenger found but no root Navigator, continuing...');
}
} catch (e) {
debugPrint('[SnackBarHelper] Strategy 3: Error checking root Navigator: $e');
}
}
}
} catch (e) {
debugPrint('[SnackBarHelper] Strategy 3: Error at depth $depth: $e');
}
// Move to parent element
final element = currentContext as Element?;
if (element != null) {
BuildContext? parentContext;
element.visitAncestorElements((parent) {
parentContext = parent;
return false; // Stop after first ancestor
});
currentContext = parentContext;
} else {
debugPrint('[SnackBarHelper] Strategy 3: Reached end of tree at depth $depth');
break;
}
}
if (foundMessenger != null) {
return foundMessenger;
}
// Fallback: try to find any ScaffoldMessenger in the current context tree
// This will work even if we're in a dialog, but might not be the root
debugPrint('[SnackBarHelper] Fallback: Trying current context...');
if (context.mounted) {
final messenger = ScaffoldMessenger.maybeOf(context);
if (messenger != null) {
debugPrint('[SnackBarHelper] ⚠ Fallback SUCCESS: Using ScaffoldMessenger from current context (may be wrong one!)');
return messenger;
} else {
debugPrint('[SnackBarHelper] ✗ Fallback FAILED: No ScaffoldMessenger in current context');
}
}
// Last resort: use ScaffoldMessenger.of which will throw if not found
// This should rarely happen in practice
debugPrint('[SnackBarHelper] ⚠ Last resort: Using ScaffoldMessenger.of (may throw)');
try {
final messenger = ScaffoldMessenger.of(context);
debugPrint('[SnackBarHelper] ⚠ Last resort SUCCESS: Got ScaffoldMessenger (may be wrong one!)');
return messenger;
} catch (e) {
debugPrint('[SnackBarHelper] ✗ Last resort ERROR: $e');
rethrow;
}
}
}