323 lines
9.7 KiB
Dart
323 lines
9.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
|
|
import '../../l10n/l10n_x.dart';
|
|
|
|
enum EmptyStateType {
|
|
noGroups,
|
|
noMembers,
|
|
noPayments,
|
|
noActivities,
|
|
noResults,
|
|
error,
|
|
noInternet,
|
|
}
|
|
|
|
class EmptyStateWidget extends StatelessWidget {
|
|
final EmptyStateType type;
|
|
final String? customTitle;
|
|
final String? customMessage;
|
|
final String? actionLabel;
|
|
final VoidCallback? onActionPressed;
|
|
final Widget? customIllustration;
|
|
|
|
const EmptyStateWidget({
|
|
super.key,
|
|
required this.type,
|
|
this.customTitle,
|
|
this.customMessage,
|
|
this.actionLabel,
|
|
this.onActionPressed,
|
|
this.customIllustration,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final config = _getEmptyStateConfig(context, type);
|
|
|
|
return Center(
|
|
child: SingleChildScrollView(
|
|
padding: EdgeInsets.all(32.w),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// Illustration or Icon
|
|
customIllustration ??
|
|
TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
duration: const Duration(milliseconds: 600),
|
|
curve: Curves.easeOutBack,
|
|
builder: (context, value, child) {
|
|
return Transform.scale(
|
|
scale: value,
|
|
child: child,
|
|
);
|
|
},
|
|
child: Container(
|
|
width: 140.w,
|
|
height: 140.h,
|
|
decoration: BoxDecoration(
|
|
color: config.backgroundColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
config.icon,
|
|
size: 70.w,
|
|
color: config.iconColor,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 32.h),
|
|
|
|
// Title
|
|
TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
duration: const Duration(milliseconds: 800),
|
|
curve: Curves.easeOut,
|
|
builder: (context, value, child) {
|
|
return Opacity(
|
|
opacity: value,
|
|
child: Transform.translate(
|
|
offset: Offset(0, 20 * (1 - value)),
|
|
child: child,
|
|
),
|
|
);
|
|
},
|
|
child: Text(
|
|
customTitle ?? config.title,
|
|
style: TextStyle(
|
|
fontSize: 24.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey.shade800,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
SizedBox(height: 12.h),
|
|
|
|
// Message
|
|
TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
duration: const Duration(milliseconds: 1000),
|
|
curve: Curves.easeOut,
|
|
builder: (context, value, child) {
|
|
return Opacity(
|
|
opacity: value,
|
|
child: Transform.translate(
|
|
offset: Offset(0, 20 * (1 - value)),
|
|
child: child,
|
|
),
|
|
);
|
|
},
|
|
child: Text(
|
|
customMessage ?? config.message,
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
color: Colors.grey.shade600,
|
|
height: 1.5,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
SizedBox(height: 32.h),
|
|
|
|
// Action Button
|
|
if (onActionPressed != null)
|
|
TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
duration: const Duration(milliseconds: 1200),
|
|
curve: Curves.easeOut,
|
|
builder: (context, value, child) {
|
|
return Opacity(
|
|
opacity: value,
|
|
child: Transform.translate(
|
|
offset: Offset(0, 20 * (1 - value)),
|
|
child: child,
|
|
),
|
|
);
|
|
},
|
|
child: ElevatedButton.icon(
|
|
onPressed: onActionPressed,
|
|
icon: Icon(config.actionIcon, size: 20.w),
|
|
label: Text(
|
|
actionLabel ?? config.actionLabel,
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: config.buttonColor,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 32.w,
|
|
vertical: 16.h,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
),
|
|
elevation: 2,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
_EmptyStateConfig _getEmptyStateConfig(BuildContext context, EmptyStateType type) {
|
|
final l = context.l10n;
|
|
switch (type) {
|
|
case EmptyStateType.noGroups:
|
|
return _EmptyStateConfig(
|
|
icon: Icons.group_add_rounded,
|
|
iconColor: Colors.green.shade600,
|
|
backgroundColor: Colors.green.shade50,
|
|
title: l.emptyNoGroupsTitle,
|
|
message: l.emptyNoGroupsMessage,
|
|
actionLabel: l.emptyNoGroupsAction,
|
|
actionIcon: Icons.add_circle_outline,
|
|
buttonColor: Colors.green.shade600,
|
|
);
|
|
|
|
case EmptyStateType.noMembers:
|
|
return _EmptyStateConfig(
|
|
icon: Icons.people_outline_rounded,
|
|
iconColor: Colors.blue.shade600,
|
|
backgroundColor: Colors.blue.shade50,
|
|
title: l.emptyNoMembersTitle,
|
|
message: l.emptyNoMembersMessage,
|
|
actionLabel: l.emptyNoMembersAction,
|
|
actionIcon: Icons.person_add,
|
|
buttonColor: Colors.blue.shade600,
|
|
);
|
|
|
|
case EmptyStateType.noPayments:
|
|
return _EmptyStateConfig(
|
|
icon: Icons.payment_rounded,
|
|
iconColor: Colors.orange.shade600,
|
|
backgroundColor: Colors.orange.shade50,
|
|
title: l.emptyNoPaymentsTitle,
|
|
message: l.emptyNoPaymentsMessage,
|
|
actionLabel: l.emptyNoPaymentsAction,
|
|
actionIcon: Icons.add,
|
|
buttonColor: Colors.orange.shade600,
|
|
);
|
|
|
|
case EmptyStateType.noActivities:
|
|
return _EmptyStateConfig(
|
|
icon: Icons.history_rounded,
|
|
iconColor: Colors.purple.shade600,
|
|
backgroundColor: Colors.purple.shade50,
|
|
title: l.emptyNoActivitiesTitle,
|
|
message: l.emptyNoActivitiesMessage,
|
|
actionLabel: l.emptyNoActivitiesAction,
|
|
actionIcon: Icons.refresh,
|
|
buttonColor: Colors.purple.shade600,
|
|
);
|
|
|
|
case EmptyStateType.noResults:
|
|
return _EmptyStateConfig(
|
|
icon: Icons.search_off_rounded,
|
|
iconColor: Colors.grey.shade600,
|
|
backgroundColor: Colors.grey.shade100,
|
|
title: l.emptyNoResultsTitle,
|
|
message: l.emptyNoResultsMessage,
|
|
actionLabel: l.emptyNoResultsAction,
|
|
actionIcon: Icons.clear_all,
|
|
buttonColor: Colors.grey.shade600,
|
|
);
|
|
|
|
case EmptyStateType.error:
|
|
return _EmptyStateConfig(
|
|
icon: Icons.error_outline_rounded,
|
|
iconColor: Colors.red.shade600,
|
|
backgroundColor: Colors.red.shade50,
|
|
title: l.emptyErrorTitle,
|
|
message: l.emptyErrorMessage,
|
|
actionLabel: l.emptyErrorAction,
|
|
actionIcon: Icons.refresh,
|
|
buttonColor: Colors.red.shade600,
|
|
);
|
|
|
|
case EmptyStateType.noInternet:
|
|
return _EmptyStateConfig(
|
|
icon: Icons.wifi_off_rounded,
|
|
iconColor: Colors.red.shade600,
|
|
backgroundColor: Colors.red.shade50,
|
|
title: l.emptyNoInternetTitle,
|
|
message: l.emptyNoInternetMessage,
|
|
actionLabel: l.emptyNoInternetAction,
|
|
actionIcon: Icons.refresh,
|
|
buttonColor: Colors.red.shade600,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
class _EmptyStateConfig {
|
|
final IconData icon;
|
|
final Color iconColor;
|
|
final Color backgroundColor;
|
|
final String title;
|
|
final String message;
|
|
final String actionLabel;
|
|
final IconData actionIcon;
|
|
final Color buttonColor;
|
|
|
|
_EmptyStateConfig({
|
|
required this.icon,
|
|
required this.iconColor,
|
|
required this.backgroundColor,
|
|
required this.title,
|
|
required this.message,
|
|
required this.actionLabel,
|
|
required this.actionIcon,
|
|
required this.buttonColor,
|
|
});
|
|
}
|
|
|
|
/// Compact version for smaller spaces
|
|
class CompactEmptyState extends StatelessWidget {
|
|
final IconData icon;
|
|
final String message;
|
|
final Color? color;
|
|
|
|
const CompactEmptyState({
|
|
super.key,
|
|
required this.icon,
|
|
required this.message,
|
|
this.color,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final stateColor = color ?? Colors.grey.shade600;
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.all(24.w),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
size: 48.w,
|
|
color: stateColor.withOpacity(0.5),
|
|
),
|
|
SizedBox(height: 12.h),
|
|
Text(
|
|
message,
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|