fixed animations

This commit is contained in:
Deep Koluguri 2026-04-05 23:43:12 -04:00
parent 5c284a3698
commit 1c861e1f4b
5 changed files with 856 additions and 522 deletions

View File

@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'app_theme.dart';
/// Visual tokens for the full-screen monthly draw / slot machine.
/// Anchored to [AppTheme] teal seed + amber tertiary; respects light/dark [ColorScheme].
class DrawSlotTheming {
DrawSlotTheming(this.scheme, this.brightness);
final ColorScheme scheme;
final Brightness brightness;
bool get _isDark => brightness == Brightness.dark;
/// Immersive page background (draw route).
LinearGradient get pageBackdrop {
if (_isDark) {
final deep = Color.alphaBlend(
scheme.primary.withOpacity(0.5),
const Color(0xFF041014),
);
final mid = Color.alphaBlend(
scheme.primary.withOpacity(0.22),
const Color(0xFF0A1C22),
);
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: const [0.0, 0.55, 1.0],
colors: [
deep,
mid,
Color.alphaBlend(scheme.surface.withOpacity(0.15), const Color(0xFF020608)),
],
);
}
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color.alphaBlend(scheme.primary.withOpacity(0.12), scheme.surface),
scheme.surface,
Color.alphaBlend(scheme.primaryContainer.withOpacity(0.35), scheme.surface),
],
);
}
Color get pageOnText =>
_isDark ? Colors.white.withOpacity(0.95) : scheme.onSurface;
Color get pageMuted =>
_isDark ? Colors.white.withOpacity(0.72) : scheme.onSurfaceVariant;
Color get chipFill => _isDark
? Colors.white.withOpacity(0.12)
: scheme.primaryContainer.withOpacity(0.45);
Color get chipBorder =>
_isDark ? Colors.white.withOpacity(0.22) : scheme.outlineVariant;
/// Pre-start intro card (on draw page).
BoxDecoration introCardDecoration() {
return BoxDecoration(
color: _isDark
? Color.alphaBlend(Colors.white.withOpacity(0.08), const Color(0xFF0C181C))
: scheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: _isDark
? Colors.white.withOpacity(0.14)
: scheme.outlineVariant,
width: 1,
),
boxShadow: [
BoxShadow(
color: scheme.primary.withOpacity(_isDark ? 0.18 : 0.08),
blurRadius: 24.r,
offset: Offset(0, 12.h),
),
],
);
}
/// Slot machine outer chassis.
BoxDecoration slotChassisDecoration() {
return BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: _isDark
? [
Color.alphaBlend(scheme.surfaceContainerHigh.withOpacity(0.4), const Color(0xFF0D1619)),
const Color(0xFF050A0C),
]
: [
scheme.surfaceContainerHigh,
scheme.surfaceContainer,
],
),
borderRadius: BorderRadius.circular(22.r),
border: Border.all(
color: Color.alphaBlend(
scheme.tertiary.withOpacity(0.85),
scheme.outline,
),
width: 2.w,
),
boxShadow: [
BoxShadow(
color: scheme.primary.withOpacity(_isDark ? 0.35 : 0.12),
blurRadius: 28.r,
spreadRadius: -2,
offset: Offset(0, 14.h),
),
BoxShadow(
color: Colors.black.withOpacity(_isDark ? 0.45 : 0.06),
blurRadius: 20.r,
offset: Offset(0, 8.h),
),
],
);
}
Color get slotInnerWell => _isDark
? const Color(0xFF020608)
: Color.alphaBlend(scheme.surfaceContainerHighest, scheme.surface);
List<Color> rowColorsIdle() => _isDark
? [
Color.alphaBlend(scheme.surfaceContainerHigh.withOpacity(0.5), const Color(0xFF1A2529)),
Color.alphaBlend(scheme.primary.withOpacity(0.35), const Color(0xFF0F181C)),
]
: [
scheme.surfaceContainerHigh,
scheme.surfaceContainer,
];
List<Color> rowColorsSpinning() => [
Color.alphaBlend(scheme.primary.withOpacity(0.55), rowColorsIdle().first),
Color.alphaBlend(scheme.primary.withOpacity(0.4), rowColorsIdle().last),
];
List<Color> rowColorsWinner() => _isDark
? [
Color.alphaBlend(scheme.primary.withOpacity(0.9), const Color(0xFF0A3030)),
Color.alphaBlend(scheme.tertiary.withOpacity(0.35), scheme.primary),
]
: [
scheme.primary,
Color.alphaBlend(scheme.tertiary.withOpacity(0.28), scheme.primary),
];
Color get winnerRowText =>
_isDark ? Colors.white : scheme.onPrimary;
Color get idleRowText => _isDark
? Colors.white.withOpacity(0.92)
: scheme.onSurface;
Color get winnerFrameColor =>
Color.alphaBlend(scheme.tertiary.withOpacity(0.95), AppTheme.accent);
BoxDecoration winnerSummaryDecoration() {
return BoxDecoration(
gradient: LinearGradient(
colors: [
Color.alphaBlend(scheme.primaryContainer.withOpacity(_isDark ? 0.35 : 0.65), scheme.surface),
Color.alphaBlend(scheme.tertiary.withOpacity(_isDark ? 0.12 : 0.2), scheme.surface),
],
),
borderRadius: BorderRadius.circular(18.r),
border: Border.all(
color: scheme.primary.withOpacity(0.35),
width: 1.5,
),
);
}
TextStyle winnerSummaryTitle(TextTheme t) => (t.titleLarge ?? const TextStyle()).copyWith(
fontWeight: FontWeight.w800,
color: scheme.primary,
height: 1.2,
);
Color get winnerSummaryIcon => scheme.tertiary;
}

View File

@ -258,6 +258,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return RecordingOverlay(
showRecordingIndicator: true,
child: Dialog(
@ -272,7 +273,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.purple.shade600,
color: scheme.primary,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.r),
topRight: Radius.circular(16.r),
@ -281,8 +282,8 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
child: Row(
children: [
Icon(
Icons.casino,
color: Colors.white,
Icons.emoji_events_rounded,
color: scheme.onPrimary,
size: 24.w,
),
SizedBox(width: 12.w),
@ -293,7 +294,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
Text(
'Monthly Draw',
style: TextStyle(
color: Colors.white,
color: scheme.onPrimary,
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
@ -301,7 +302,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
Text(
'${_monthController.text}/${_yearController.text}',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
color: scheme.onPrimary.withOpacity(0.9),
fontSize: 12.sp,
),
),
@ -310,7 +311,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, color: Colors.white),
icon: Icon(Icons.close, color: scheme.onPrimary),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
@ -342,9 +343,12 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
color: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(color: Colors.grey.shade200, width: 1),
top: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
width: 1,
),
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(16.r),
@ -374,11 +378,11 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
child: ElevatedButton(
onPressed: _isLoading ? null : _startDraw,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple.shade600,
foregroundColor: Colors.white,
backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
borderRadius: BorderRadius.circular(12.r),
),
),
child: Row(
@ -406,14 +410,14 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
),
if (_isDrawComplete)
Expanded(
child: ElevatedButton(
child: FilledButton(
onPressed: _saveDrawResult,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade600,
foregroundColor: Colors.white,
style: FilledButton.styleFrom(
backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
borderRadius: BorderRadius.circular(12.r),
),
),
child: Row(
@ -440,6 +444,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
}
Widget _buildDrawForm() {
final scheme = Theme.of(context).colorScheme;
return Form(
key: _formKey,
child: Column(
@ -451,7 +456,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
color: scheme.onSurface,
),
),
SizedBox(height: 16.h),
@ -555,14 +560,16 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
height: 16.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.purple.shade600),
valueColor: AlwaysStoppedAnimation<Color>(scheme.primary),
),
)
else
Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h),
decoration: BoxDecoration(
color: _eligibleMembers.isEmpty ? Colors.orange.shade100 : Colors.purple.shade100,
color: _eligibleMembers.isEmpty
? Colors.orange.shade100
: scheme.primaryContainer,
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
@ -570,7 +577,9 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: _eligibleMembers.isEmpty ? Colors.orange.shade800 : Colors.purple.shade800,
color: _eligibleMembers.isEmpty
? Colors.orange.shade800
: scheme.onPrimaryContainer,
),
),
),
@ -637,12 +646,12 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
return ListTile(
dense: true,
leading: CircleAvatar(
backgroundColor: Colors.purple.shade100,
backgroundColor: scheme.primaryContainer,
radius: 18.r,
child: Text(
(member['name']?.isNotEmpty == true ? member['name'].substring(0, 1) : 'M').toUpperCase(),
style: TextStyle(
color: Colors.purple.shade700,
color: scheme.primary,
fontWeight: FontWeight.w600,
fontSize: 14.sp,
),
@ -660,8 +669,8 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
style: TextStyle(fontSize: 12.sp),
),
trailing: Icon(
Icons.check_circle,
color: Colors.green.shade600,
Icons.check_circle_rounded,
color: scheme.primary,
size: 18.w,
),
);
@ -679,21 +688,22 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
}
Widget _buildDrawResult() {
final scheme = Theme.of(context).colorScheme;
return Column(
children: [
// Winner Announcement
Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: Colors.green.shade200),
color: scheme.primaryContainer.withOpacity(0.65),
borderRadius: BorderRadius.circular(16.r),
border: Border.all(color: scheme.primary.withOpacity(0.25)),
),
child: Column(
children: [
Icon(
Icons.emoji_events,
color: Colors.amber.shade600,
Icons.emoji_events_rounded,
color: scheme.tertiary,
size: 48.w,
),
SizedBox(height: 16.h),
@ -702,7 +712,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: Colors.green.shade800,
color: scheme.primary,
),
),
SizedBox(height: 8.h),
@ -711,7 +721,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.green.shade700,
color: scheme.onSurface,
),
),
SizedBox(height: 4.h),
@ -719,7 +729,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
_winnerData?['mobile'] ?? '',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
color: scheme.onSurfaceVariant,
),
),
],
@ -731,20 +741,21 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.blue.shade200),
color: scheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: scheme.outlineVariant),
),
child: Row(
children: [
Icon(Icons.security, color: Colors.blue.shade600, size: 16.w),
Icon(Icons.verified_user_rounded, color: scheme.primary, size: 18.w),
SizedBox(width: 8.w),
Expanded(
child: Text(
'Draw completed using provably fair system. Recording saved.',
style: TextStyle(
fontSize: 12.sp,
color: Colors.blue.shade700,
color: scheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
@ -771,11 +782,12 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
if (created != null) {
Navigator.of(context).pop();
final s = Theme.of(context).colorScheme;
Get.snackbar(
'Success',
'Monthly draw completed successfully!',
backgroundColor: Colors.green.shade600,
colorText: Colors.white,
backgroundColor: s.primary,
colorText: s.onPrimary,
snackPosition: SnackPosition.TOP,
);
} else {

View File

@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../core/services/chit_group_service.dart';
import '../../core/models/chit_group.dart';
import '../../core/themes/draw_slot_theme.dart';
import '../../shared/widgets/draw_animation_selector.dart';
import '../../core/utils/whatsapp_util.dart';
@ -34,9 +35,81 @@ class _DrawAnimationPageState extends State<DrawAnimationPage>
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
bool _isComplete = false;
/// True after the draw is successfully persisted (safe to leave without prompt).
bool _drawRecorded = false;
/// Single client seed for the whole page so animation + API save use the same value.
late final String _animationClientSeed;
void _exitDrawScreen({required bool recorded}) {
if (!mounted) return;
Navigator.of(context).pop(recorded);
}
/// System back / header always offers a way out.
Future<void> _handleLeaveIntent() async {
if (!_isComplete) {
final cancel = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Cancel draw?'),
content: const Text(
'The draw is still in progress. Stop and go back?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Keep drawing'),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
style: FilledButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
),
child: const Text('Stop'),
),
],
),
);
if (cancel == true && mounted) {
_exitDrawScreen(recorded: false);
}
return;
}
if (_drawRecorded) {
_exitDrawScreen(recorded: true);
return;
}
final leave = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Leave without saving?'),
content: const Text(
'This winner is not recorded yet. If you leave now, you will need to run the draw again.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Stay'),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
style: FilledButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
),
child: const Text('Leave anyway'),
),
],
),
);
if (leave == true && mounted) {
_exitDrawScreen(recorded: false);
}
}
@override
void initState() {
super.initState();
@ -104,8 +177,7 @@ class _DrawAnimationPageState extends State<DrawAnimationPage>
if (shouldSave == true) {
await _saveDrawResult(winnerId, bidAmount);
} else {
// User cancelled, go back without saving
Get.back(result: false);
_exitDrawScreen(recorded: false);
}
}
@ -162,6 +234,11 @@ class _DrawAnimationPageState extends State<DrawAnimationPage>
),
),
),
IconButton(
tooltip: 'Close',
onPressed: () => Navigator.pop(dialogContext, false),
icon: Icon(Icons.close, color: Colors.grey.shade700),
),
],
),
SizedBox(height: 8.h),
@ -429,7 +506,6 @@ _Congratulations to the winner!_
Get.back();
if (created == null) {
Get.back(result: false);
Get.snackbar(
'Error',
'Failed to save draw result',
@ -441,6 +517,10 @@ _Congratulations to the winner!_
return;
}
setState(() {
_drawRecorded = true;
});
final drawId = created['id']?.toString();
if (drawId != null) {
final publicUrl = await WhatsAppUtil.getDrawPublicShareUrl(drawId);
@ -459,7 +539,7 @@ _Congratulations to the winner!_
}
if (!mounted) return;
Get.back(result: true);
_exitDrawScreen(recorded: true);
Get.snackbar(
'Draw Saved! 🎉',
@ -470,47 +550,63 @@ _Congratulations to the winner!_
snackPosition: SnackPosition.TOP,
);
} catch (e) {
if (mounted) Get.back();
if (mounted) {
try {
Get.back();
} catch (_) {}
}
Get.back(result: false);
Get.snackbar(
'Error',
'Failed to save draw result: ${e.toString()}',
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 4),
);
if (mounted) {
Get.snackbar(
'Error',
'Failed to save draw result: ${e.toString()}',
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 4),
);
}
}
}
Future<void> _showPublicResultLinkDialog(String url) async {
await showDialog<void>(
context: context,
barrierDismissible: false,
barrierDismissible: true,
builder: (ctx) => AlertDialog(
title: const Text('Public result link'),
content: SelectableText(
url,
style: TextStyle(fontSize: 13.sp),
title: Row(
children: [
Expanded(child: const Text('Public result link')),
IconButton(
tooltip: 'Close',
onPressed: () => Navigator.of(ctx).pop(),
icon: const Icon(Icons.close),
),
],
),
content: SingleChildScrollView(
child: SelectableText(
url,
style: TextStyle(fontSize: 13.sp),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Close'),
),
FilledButton(
onPressed: () async {
await Clipboard.setData(ClipboardData(text: url));
if (ctx.mounted) Navigator.of(ctx).pop();
Get.snackbar(
'Copied',
'Link copied — anyone can open it in a browser',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 3),
);
if (ctx.mounted) {
Get.snackbar(
'Copied',
'Link copied — anyone can open it in a browser',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 3),
);
}
},
child: const Text('Copy link'),
child: const Text('Copy only'),
),
FilledButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Done'),
),
],
),
@ -558,40 +654,17 @@ _Congratulations to the winner!_
];
final drawPeriodShort =
'${monthLabels[widget.month - 1]} ${widget.year}';
final theme = Theme.of(context);
final d = DrawSlotTheming(theme.colorScheme, theme.brightness);
return PopScope(
canPop: _isComplete,
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop || !mounted) return;
final leave = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Leave draw?'),
content: const Text(
'The result is not saved yet. Leave anyway?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Stay'),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
style: FilledButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
),
child: const Text('Leave'),
),
],
),
);
if (leave == true && mounted) {
Get.back(result: false);
}
await _handleLeaveIntent();
},
child: Scaffold(
backgroundColor: Colors.black,
backgroundColor: Colors.transparent,
body: SafeArea(
child: FadeTransition(
opacity: _fadeAnimation,
@ -599,93 +672,63 @@ _Congratulations to the winner!_
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.purple.shade900,
Colors.blue.shade900,
Colors.indigo.shade900,
],
),
gradient: d.pageBackdrop,
),
child: Column(
children: [
// Header
Padding(
padding: EdgeInsets.all(20.w),
padding: EdgeInsets.fromLTRB(8.w, 12.h, 12.w, 8.h),
child: Column(
children: [
Row(
children: [
if (!_isComplete)
IconButton(
icon: Icon(
Icons.close,
color: Colors.white.withOpacity(0.8),
size: 28.w,
),
onPressed: () async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Cancel Draw?'),
content: const Text('Are you sure you want to cancel the draw?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Continue'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Cancel'),
),
],
),
);
if (result == true) {
Get.back(result: false);
}
},
)
else
SizedBox(width: 48.w),
IconButton(
tooltip: _drawRecorded
? 'Done'
: (_isComplete
? 'Close'
: 'Cancel draw'),
icon: Icon(
_drawRecorded
? Icons.check_circle_outline_rounded
: Icons.close_rounded,
color: d.pageOnText,
size: 28.w,
),
onPressed: () => _handleLeaveIntent(),
),
Expanded(
child: Column(
children: [
Text(
widget.group.name,
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
style: theme.textTheme.headlineSmall?.copyWith(
color: d.pageOnText,
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8.h),
SizedBox(height: 10.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 6.h,
horizontal: 14.w,
vertical: 8.h,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
color: d.chipFill,
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: Colors.white.withOpacity(0.3),
),
border: Border.all(color: d.chipBorder),
),
child: Text(
drawPeriodShort,
style: TextStyle(
fontSize: 15.sp,
style: theme.textTheme.labelLarge?.copyWith(
color: d.pageOnText,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.2,
),
),
),
@ -717,51 +760,60 @@ _Congratulations to the winner!_
// Footer
Padding(
padding: EdgeInsets.all(20.w),
padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 16.h),
child: Column(
children: [
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Colors.white.withOpacity(0.2),
),
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 14.h,
),
child: Column(
decoration: BoxDecoration(
color: d.chipFill,
borderRadius: BorderRadius.circular(18.r),
border: Border.all(color: d.chipBorder),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildInfoItem(
'Eligible Members',
'${widget.eligibleMembers.length}',
Icons.people,
),
Container(
width: 1,
height: 30.h,
color: Colors.white.withOpacity(0.3),
),
_buildInfoItem(
'Total Members',
'${widget.group.maxMembers}',
Icons.group,
),
],
_buildInfoItem(
d,
'In this draw',
'${widget.eligibleMembers.length}',
Icons.how_to_reg_rounded,
),
Container(
width: 1,
height: 36.h,
color: d.chipBorder.withOpacity(0.6),
),
_buildInfoItem(
d,
'Group size',
'${widget.group.maxMembers}',
Icons.groups_rounded,
),
],
),
),
SizedBox(height: 12.h),
Text(
'🎲 Provably Fair Draw',
style: TextStyle(
fontSize: 14.sp,
color: Colors.white.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
SizedBox(height: 14.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.verified_user_rounded,
size: 16.w,
color: theme.colorScheme.tertiary,
),
SizedBox(width: 8.w),
Text(
'Provably fair · transparent draw',
style: theme.textTheme.bodyMedium?.copyWith(
color: d.pageMuted,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
@ -775,28 +827,35 @@ _Congratulations to the winner!_
);
}
Widget _buildInfoItem(String label, String value, IconData icon) {
Widget _buildInfoItem(
DrawSlotTheming d,
String label,
String value,
IconData icon,
) {
return Column(
children: [
Icon(
icon,
color: Colors.white.withOpacity(0.8),
size: 24.w,
color: d.pageMuted,
size: 22.w,
),
SizedBox(height: 6.h),
Text(
value,
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
fontWeight: FontWeight.w800,
color: d.pageOnText,
),
),
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: Colors.white.withOpacity(0.7),
fontSize: 11.sp,
fontWeight: FontWeight.w600,
color: d.pageMuted,
letterSpacing: 0.2,
),
),
],

View File

@ -4,6 +4,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:math' as math;
import 'dart:async';
import '../../core/themes/draw_slot_theme.dart';
// Base class for all draw animations
abstract class DrawAnimation extends StatefulWidget {
final List<Map<String, dynamic>> members;
@ -632,6 +634,10 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scheme = theme.colorScheme;
final d = DrawSlotTheming(scheme, theme.brightness);
String? winnerName;
if (_winnerId != null && _winnerId!.isNotEmpty) {
for (final m in widget.members) {
@ -644,240 +650,313 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
final maxW = MediaQuery.sizeOf(context).width - 40.w;
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxW.clamp(280.w, 400.w)),
child: SizedBox(
width: double.infinity,
height: 480.h.clamp(380.h, 560.h),
child: Column(
children: [
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.grey.shade900,
Colors.grey.shade800,
return Semantics(
label: 'Slot machine draw',
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxW.clamp(280.w, 400.w)),
child: SizedBox(
width: double.infinity,
height: 480.h.clamp(380.h, 560.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_isAnimating)
Padding(
padding: EdgeInsets.only(bottom: 10.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 15.w,
height: 15.w,
child: CircularProgressIndicator(
strokeWidth: 2.2,
color: scheme.tertiary,
),
),
SizedBox(width: 10.w),
Text(
'Selecting winner…',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
color: scheme.onSurface,
),
),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.orange.shade400, width: 3.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.35),
blurRadius: 18.r,
offset: Offset(0, 10.h),
),
],
),
Expanded(
child: Container(
margin: EdgeInsets.all(18.w),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12.r),
),
child: LayoutBuilder(
builder: (context, constraints) {
final rowH = constraints.maxHeight / 7;
return Stack(
clipBehavior: Clip.hardEdge,
children: [
AnimatedBuilder(
animation: _slotAnimation,
builder: (context, child) {
return Column(
children: List.generate(7, (index) {
final displayIndex =
index < _displayNames.length ? index : 0;
final name = _displayNames[displayIndex];
final isWinner = _isComplete && index == 3;
final isCenterHighlight =
_isAnimating && index == 3;
width: double.infinity,
decoration: d.slotChassisDecoration(),
child: Padding(
padding: EdgeInsets.all(14.w),
child: DecoratedBox(
decoration: BoxDecoration(
color: d.slotInnerWell,
borderRadius: BorderRadius.circular(14.r),
border: Border.all(
color: scheme.outline.withOpacity(0.28),
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(13.r),
child: LayoutBuilder(
builder: (context, constraints) {
final rowH = constraints.maxHeight / 7;
return Stack(
clipBehavior: Clip.hardEdge,
fit: StackFit.expand,
children: [
AnimatedBuilder(
animation: _slotAnimation,
builder: (context, child) {
return Column(
children:
List.generate(7, (index) {
final displayIndex = index <
_displayNames.length
? index
: 0;
final name =
_displayNames[displayIndex];
final isWinner =
_isComplete && index == 3;
final isCenterHighlight =
_isAnimating && index == 3;
return Expanded(
child: AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
final double scale =
isWinner || isCenterHighlight
? 1.08
: 1.0;
final double fontSize = isWinner
? 24.sp
: isCenterHighlight
? 20.sp
: 18.sp;
final FontWeight weight = isWinner
? FontWeight.w900
: isCenterHighlight
? FontWeight.w800
: FontWeight.w700;
final List<Color> colors = isWinner
? [
Colors.green.shade500,
Colors.green.shade600,
]
: isCenterHighlight
? [
Colors.deepPurple.shade500,
Colors.deepPurple.shade700,
]
: [
Colors.blueGrey.shade700,
Colors.blueGrey.shade900,
];
return Expanded(
child: AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
final scale = isWinner ||
isCenterHighlight
? 1.05
: 1.0;
List<Color> rowColors;
if (isWinner) {
rowColors = d.rowColorsWinner();
} else if (isCenterHighlight) {
rowColors =
d.rowColorsSpinning();
} else {
rowColors = d.rowColorsIdle();
}
return AnimatedContainer(
duration: const Duration(
milliseconds: 220),
curve: Curves.easeInOut,
transform: Matrix4.identity()
..scale(scale),
child: Container(
width: double.infinity,
margin: EdgeInsets.symmetric(
vertical: 6.h,
horizontal: 12.w,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: colors,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius:
BorderRadius.circular(10.r),
border: Border.all(
color: Colors.white.withOpacity(
isWinner ||
isCenterHighlight
? 0.7
: 0.15),
width: isWinner ||
isCenterHighlight
? 2.w
: 1.w,
),
boxShadow: [
BoxShadow(
color: Colors.black
.withOpacity(0.4),
blurRadius: isWinner
? 14.r
: 6.r,
offset: Offset(0, 3.h),
),
],
),
child: Center(
child: Text(
name.length > 22
? '${name.substring(0, 22)}'
: name,
style: TextStyle(
fontSize: fontSize,
fontWeight: weight,
color: Colors.white,
letterSpacing: 0.6,
shadows: [
Shadow(
color: Colors.black
.withOpacity(0.5),
blurRadius: 4.r,
offset:
const Offset(1.5, 1.5),
final borderColor = isWinner
? scheme.tertiary
.withOpacity(0.9)
: isCenterHighlight
? scheme.primary
.withOpacity(0.55)
: scheme.outline
.withOpacity(0.22);
return AnimatedContainer(
duration: const Duration(
milliseconds: 220),
curve: Curves.easeInOut,
transform: Matrix4.identity()
..scale(scale),
child: Container(
width: double.infinity,
margin:
EdgeInsets.symmetric(
vertical: 4.h,
horizontal: 10.w,
),
decoration: BoxDecoration(
gradient:
LinearGradient(
colors: rowColors,
begin: Alignment
.centerLeft,
end: Alignment
.centerRight,
),
],
borderRadius:
BorderRadius
.circular(12.r),
border: Border.all(
color: borderColor,
width: isWinner
? 2.w
: 1.w,
),
boxShadow: [
if (isWinner)
BoxShadow(
color: scheme
.primary
.withOpacity(
0.45),
blurRadius: 16.r,
offset: Offset(
0, 4.h),
),
],
),
child: Center(
child: Text(
name.length > 22
? '${name.substring(0, 22)}'
: name,
style: theme
.textTheme
.titleMedium
?.copyWith(
fontSize: isWinner
? 21.sp
: isCenterHighlight
? 18.sp
: 16.sp,
fontWeight: isWinner
? FontWeight.w900
: FontWeight
.w700,
color: isWinner
? d
.winnerRowText
: d.idleRowText,
letterSpacing: 0.2,
shadows: theme
.brightness ==
Brightness
.dark
? [
Shadow(
color: Colors
.black
.withOpacity(
0.45),
blurRadius:
3,
offset:
const Offset(
0,
1),
),
]
: null,
),
textAlign:
TextAlign.center,
maxLines: 1,
overflow: TextOverflow
.ellipsis,
),
),
),
textAlign: TextAlign.center,
maxLines: 1,
overflow:
TextOverflow.ellipsis,
),
),
);
},
),
);
},
),
);
}),
);
},
),
if (_isComplete)
Positioned(
top: rowH * 3,
left: 0,
right: 0,
height: rowH,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.r),
border: Border.all(
color: Colors.amber.shade400,
width: 2.5.w,
),
boxShadow: [
BoxShadow(
color: Colors.amber.withOpacity(0.35),
blurRadius: 14.r,
spreadRadius: 0.5,
}),
);
},
),
Positioned(
top: rowH * 2.5,
left: 8.w,
right: 8.w,
height: 1,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
scheme.tertiary.withOpacity(0.45),
Colors.transparent,
],
),
],
),
),
),
),
),
],
);
},
),
),
),
),
SizedBox(height: 16.h),
if (_isComplete && winnerName != null)
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.green.shade50,
Colors.green.shade100.withOpacity(0.85),
],
),
borderRadius: BorderRadius.circular(14.r),
border: Border.all(color: Colors.green.shade200, width: 1.5),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.emoji_events, color: Colors.amber.shade700, size: 26.w),
SizedBox(width: 10.w),
Flexible(
child: Text(
winnerName,
style: TextStyle(
fontSize: 19.sp,
fontWeight: FontWeight.w800,
color: Colors.green.shade900,
height: 1.2,
Positioned(
top: rowH * 4.5,
left: 8.w,
right: 8.w,
height: 1,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
scheme.tertiary.withOpacity(0.45),
Colors.transparent,
],
),
),
),
),
if (_isComplete)
Positioned(
top: rowH * 3,
left: 0,
right: 0,
height: rowH,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(12.r),
border: Border.all(
color: d.winnerFrameColor,
width: 2.5.w,
),
boxShadow: [
BoxShadow(
color: scheme.tertiary
.withOpacity(0.35),
blurRadius: 18.r,
spreadRadius: 0.5,
),
],
),
),
),
),
],
);
},
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
],
SizedBox(height: 14.h),
if (_isComplete && winnerName != null)
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 16.w, vertical: 16.h),
decoration: d.winnerSummaryDecoration(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.emoji_events_rounded,
color: d.winnerSummaryIcon,
size: 28.w,
),
SizedBox(width: 12.w),
Flexible(
child: Text(
winnerName,
style: d.winnerSummaryTitle(theme.textTheme)
.copyWith(fontSize: 18.sp),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
),
),
),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../core/themes/draw_slot_theme.dart';
import 'alternative_draw_animations.dart';
class DrawAnimationSelector extends StatefulWidget {
@ -47,95 +48,99 @@ class _DrawAnimationSelectorState extends State<DrawAnimationSelector> {
});
}
Widget _buildAnimationSelector() {
Widget _buildAnimationSelector(BuildContext context) {
final theme = Theme.of(context);
final scheme = theme.colorScheme;
final d = DrawSlotTheming(scheme, theme.brightness);
return Container(
padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10.r,
offset: Offset(0, 5.h),
),
],
),
padding: EdgeInsets.fromLTRB(22.w, 26.h, 22.w, 24.h),
decoration: d.introCardDecoration(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 72.w,
height: 72.w,
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(20.r),
),
child: Icon(
Icons.casino,
color: Colors.purple.shade600,
size: 36.w,
),
),
SizedBox(height: 16.h),
Text(
'Slot Machine Draw',
style: TextStyle(
fontSize: 22.sp,
fontWeight: FontWeight.w700,
color: Colors.grey.shade800,
),
textAlign: TextAlign.center,
),
SizedBox(height: 12.h),
Text(
'Our signature animation for dramatic, high-energy winner reveals.',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
height: 1.4,
),
textAlign: TextAlign.center,
),
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'Members in draw: ${widget.members.length}',
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w600,
color: Colors.purple.shade600,
),
),
),
SizedBox(height: 24.h),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _startDraw,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple.shade600,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
Row(
children: [
Container(
width: 56.w,
height: 56.w,
decoration: BoxDecoration(
color: scheme.primaryContainer.withOpacity(
theme.brightness == Brightness.dark ? 0.35 : 0.65),
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(
Icons.auto_awesome_rounded,
color: scheme.primary,
size: 28.w,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.play_arrow, size: 20.w),
SizedBox(width: 8.w),
Text(
'Start Slot Machine',
style: TextStyle(fontSize: 16.sp),
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Monthly draw',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w800,
letterSpacing: -0.2,
),
),
SizedBox(height: 4.h),
Text(
'Fair random pick with a clear on-screen reveal.',
style: theme.textTheme.bodyMedium?.copyWith(
height: 1.35,
),
),
],
),
),
],
),
SizedBox(height: 20.h),
Container(
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.h),
decoration: BoxDecoration(
color: scheme.surfaceContainerHighest.withOpacity(
theme.brightness == Brightness.dark ? 0.5 : 1),
borderRadius: BorderRadius.circular(14.r),
border: Border.all(color: scheme.outlineVariant),
),
child: Row(
children: [
Icon(
Icons.people_alt_rounded,
size: 20.w,
color: scheme.primary,
),
SizedBox(width: 10.w),
Expanded(
child: Text(
'${widget.members.length} eligible members in this draw',
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
],
),
),
SizedBox(height: 22.h),
FilledButton.icon(
onPressed: _startDraw,
icon: Icon(Icons.play_arrow_rounded, size: 22.w),
label: Text(
'Start draw',
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
style: FilledButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
),
),
@ -153,7 +158,7 @@ class _DrawAnimationSelectorState extends State<DrawAnimationSelector> {
width: maxW,
child: Column(
children: [
if (!_isDrawStarted) _buildAnimationSelector(),
if (!_isDrawStarted) _buildAnimationSelector(context),
if (_isDrawStarted) ...[
SlotMachineDrawAnimation(
members: widget.members,
@ -167,28 +172,20 @@ class _DrawAnimationSelectorState extends State<DrawAnimationSelector> {
if (widget.allowReplay)
SizedBox(
width: double.infinity,
child: OutlinedButton(
child: OutlinedButton.icon(
onPressed: _resetDraw,
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.refresh, size: 18.w, color: Colors.purple.shade600),
SizedBox(width: 6.w),
Text(
'Spin Again',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.purple.shade600,
icon: Icon(Icons.replay_rounded, size: 20.w),
label: Text(
'Run again',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
],
),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
),
),
),