From 1c861e1f4b4df46a34e2b46bbcb9bb308dec0250 Mon Sep 17 00:00:00 2001 From: Deep Koluguri Date: Sun, 5 Apr 2026 23:43:12 -0400 Subject: [PATCH] fixed animations --- .../lib/core/themes/draw_slot_theme.dart | 187 +++++++ .../manager/combined_draw_dialog.dart | 90 +-- .../manager/draw_animation_page.dart | 385 +++++++------ .../widgets/alternative_draw_animations.dart | 511 ++++++++++-------- .../widgets/draw_animation_selector.dart | 205 ++++--- 5 files changed, 856 insertions(+), 522 deletions(-) create mode 100644 luckychit/lib/core/themes/draw_slot_theme.dart diff --git a/luckychit/lib/core/themes/draw_slot_theme.dart b/luckychit/lib/core/themes/draw_slot_theme.dart new file mode 100644 index 0000000..f8c4d31 --- /dev/null +++ b/luckychit/lib/core/themes/draw_slot_theme.dart @@ -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 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 rowColorsSpinning() => [ + Color.alphaBlend(scheme.primary.withOpacity(0.55), rowColorsIdle().first), + Color.alphaBlend(scheme.primary.withOpacity(0.4), rowColorsIdle().last), + ]; + + List 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; +} diff --git a/luckychit/lib/interfaces/manager/combined_draw_dialog.dart b/luckychit/lib/interfaces/manager/combined_draw_dialog.dart index 980b4e8..dc2a1bd 100644 --- a/luckychit/lib/interfaces/manager/combined_draw_dialog.dart +++ b/luckychit/lib/interfaces/manager/combined_draw_dialog.dart @@ -258,6 +258,7 @@ class _CombinedDrawDialogState extends State @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 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 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 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 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 ), 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 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 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 ), 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 } Widget _buildDrawForm() { + final scheme = Theme.of(context).colorScheme; return Form( key: _formKey, child: Column( @@ -451,7 +456,7 @@ class _CombinedDrawDialogState extends State 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 height: 16.w, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.purple.shade600), + valueColor: AlwaysStoppedAnimation(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 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 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 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 } 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 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 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 _winnerData?['mobile'] ?? '', style: TextStyle( fontSize: 14.sp, - color: Colors.grey.shade600, + color: scheme.onSurfaceVariant, ), ), ], @@ -731,20 +741,21 @@ class _CombinedDrawDialogState extends State 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 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 { diff --git a/luckychit/lib/interfaces/manager/draw_animation_page.dart b/luckychit/lib/interfaces/manager/draw_animation_page.dart index d418d3e..9612806 100644 --- a/luckychit/lib/interfaces/manager/draw_animation_page.dart +++ b/luckychit/lib/interfaces/manager/draw_animation_page.dart @@ -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 late AnimationController _fadeController; late Animation _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 _handleLeaveIntent() async { + if (!_isComplete) { + final cancel = await showDialog( + 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( + 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 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 ), ), ), + 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 _showPublicResultLinkDialog(String url) async { await showDialog( 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( - 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( - 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, ), ), ], diff --git a/luckychit/lib/shared/widgets/alternative_draw_animations.dart b/luckychit/lib/shared/widgets/alternative_draw_animations.dart index 4d2dd23..03b25ff 100644 --- a/luckychit/lib/shared/widgets/alternative_draw_animations.dart +++ b/luckychit/lib/shared/widgets/alternative_draw_animations.dart @@ -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> members; @@ -632,6 +634,10 @@ class _SlotMachineDrawAnimationState extends State @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 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 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 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, + ), + ), + ], + ), + ), + ], + ), ), ), ), diff --git a/luckychit/lib/shared/widgets/draw_animation_selector.dart b/luckychit/lib/shared/widgets/draw_animation_selector.dart index bfff320..da77962 100644 --- a/luckychit/lib/shared/widgets/draw_animation_selector.dart +++ b/luckychit/lib/shared/widgets/draw_animation_selector.dart @@ -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 { }); } - 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 { 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 { 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), + ), ), ), ),