262 lines
7.3 KiB
Dart
262 lines
7.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../core/theme/app_theme.dart';
|
|
|
|
enum NotificationType { success, error, warning, info }
|
|
|
|
class DialogService {
|
|
// Singleton pattern
|
|
static final DialogService _instance = DialogService._internal();
|
|
|
|
factory DialogService() {
|
|
return _instance;
|
|
}
|
|
|
|
DialogService._internal();
|
|
|
|
// Global keys
|
|
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
|
|
GlobalKey<ScaffoldMessengerState>();
|
|
|
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
|
|
|
// Helper method to get context (use with caution, better to pass context or use keys)
|
|
BuildContext? get _context => navigatorKey.currentContext;
|
|
|
|
// --- SnackBar Notifications ---
|
|
|
|
void showSnackBar({
|
|
required String message,
|
|
NotificationType type = NotificationType.info,
|
|
Duration duration = const Duration(seconds: 3),
|
|
String? actionLabel,
|
|
VoidCallback? onAction,
|
|
}) {
|
|
if (scaffoldMessengerKey.currentState == null) {
|
|
debugPrint(
|
|
'⚠️ ScaffoldMessengerState is null, cannot show SnackBar: $message',
|
|
);
|
|
return;
|
|
}
|
|
|
|
Color backgroundColor;
|
|
Color textColor = Colors.white;
|
|
IconData icon;
|
|
|
|
switch (type) {
|
|
case NotificationType.success:
|
|
backgroundColor = AppTheme.success;
|
|
icon = Icons.check_circle_outline;
|
|
break;
|
|
case NotificationType.error:
|
|
backgroundColor = AppTheme.error.withOpacity(0.9);
|
|
icon = Icons.error_outline;
|
|
break;
|
|
case NotificationType.warning:
|
|
backgroundColor = AppTheme.warning;
|
|
textColor = Colors.black87; // Dark text for yellow background
|
|
icon = Icons.warning_amber_rounded;
|
|
break;
|
|
case NotificationType.info:
|
|
backgroundColor = AppTheme.primaryBrown;
|
|
icon = Icons.info_outline;
|
|
break;
|
|
}
|
|
|
|
scaffoldMessengerKey.currentState!.removeCurrentSnackBar();
|
|
scaffoldMessengerKey.currentState!.showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: [
|
|
Icon(icon, color: textColor, size: 24),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
message,
|
|
style: TextStyle(
|
|
color: textColor,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
fontFamily: 'Poppins',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
backgroundColor: backgroundColor,
|
|
duration: duration,
|
|
behavior: SnackBarBehavior.floating,
|
|
margin: const EdgeInsets.all(16),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
action: actionLabel != null
|
|
? SnackBarAction(
|
|
label: actionLabel,
|
|
textColor: textColor,
|
|
onPressed: onAction ?? () {},
|
|
)
|
|
: null,
|
|
),
|
|
);
|
|
}
|
|
|
|
void showSuccess(String message) {
|
|
showSnackBar(message: message, type: NotificationType.success);
|
|
}
|
|
|
|
void showError(String message) {
|
|
showSnackBar(message: message, type: NotificationType.error);
|
|
}
|
|
|
|
void showWarning(String message) {
|
|
showSnackBar(message: message, type: NotificationType.warning);
|
|
}
|
|
|
|
void showInfo(String message) {
|
|
showSnackBar(message: message, type: NotificationType.info);
|
|
}
|
|
|
|
// --- Dialogs ---
|
|
|
|
Future<void> showAlertDialog({
|
|
required String title,
|
|
required String message,
|
|
String buttonText = 'OK',
|
|
BuildContext? context,
|
|
}) async {
|
|
final ctx = context ?? _context;
|
|
if (ctx == null) return;
|
|
|
|
return showDialog(
|
|
context: ctx,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text(
|
|
title,
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
content: Text(message),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
child: Text(buttonText),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<bool> showConfirmDialog({
|
|
required String title,
|
|
required String message,
|
|
String confirmText = 'Ya',
|
|
String cancelText = 'Batal',
|
|
bool isDanger = false,
|
|
BuildContext? context,
|
|
}) async {
|
|
final ctx = context ?? _context;
|
|
if (ctx == null) return false;
|
|
|
|
return await showDialog<bool>(
|
|
context: ctx,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text(
|
|
title,
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
content: Text(message),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
child: Text(
|
|
cancelText,
|
|
style: const TextStyle(color: Colors.grey),
|
|
),
|
|
onPressed: () {
|
|
Navigator.of(context).pop(false);
|
|
},
|
|
),
|
|
TextButton(
|
|
child: Text(
|
|
confirmText,
|
|
style: TextStyle(
|
|
color: isDanger ? AppTheme.error : AppTheme.primaryBrown,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
onPressed: () {
|
|
Navigator.of(context).pop(true);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
) ??
|
|
false;
|
|
}
|
|
|
|
Future<void> showLoadingDialog({
|
|
String message = 'Memuat...',
|
|
BuildContext? context,
|
|
}) async {
|
|
final ctx = context ?? _context;
|
|
if (ctx == null) return;
|
|
|
|
showDialog(
|
|
context: ctx,
|
|
barrierDismissible: false,
|
|
builder: (BuildContext context) {
|
|
return PopScope(
|
|
canPop: false, // Prevent back button
|
|
child: Dialog(
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const CircularProgressIndicator(color: AppTheme.primaryBrown),
|
|
const SizedBox(height: 20),
|
|
Text(
|
|
message,
|
|
style: const TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void hideLoadingDialog({BuildContext? context}) {
|
|
final ctx = context ?? _context;
|
|
if (ctx == null) {
|
|
// If we don't have context but assume the navigator is attached
|
|
if (navigatorKey.currentState?.canPop() ?? false) {
|
|
navigatorKey.currentState?.pop();
|
|
}
|
|
return;
|
|
}
|
|
|
|
Navigator.of(ctx).pop();
|
|
}
|
|
}
|