diff --git a/luckychit/lib/interfaces/manager/combined_draw_dialog.dart b/luckychit/lib/interfaces/manager/combined_draw_dialog.dart index 01446ca..378e6e9 100644 --- a/luckychit/lib/interfaces/manager/combined_draw_dialog.dart +++ b/luckychit/lib/interfaces/manager/combined_draw_dialog.dart @@ -7,6 +7,7 @@ import '../../core/models/chit_group.dart'; import '../../shared/widgets/draw_animation_selector.dart'; import '../../shared/widgets/recording_overlay.dart'; import '../../core/services/screen_recording_service.dart'; +import 'draw_animation_page.dart'; class CombinedDrawDialog extends StatefulWidget { final ChitGroup group; @@ -130,13 +131,36 @@ class _CombinedDrawDialogState extends State } } - void _startDraw() { + void _startDraw() async { if (!_formKey.currentState!.validate()) return; - setState(() { - _isDrawStarted = true; - _generateSeeds(); - }); + // Generate seeds before navigation + _generateSeeds(); + + final month = int.parse(_monthController.text); + final year = int.parse(_yearController.text); + + // Close this dialog and navigate to full-screen draw animation + Navigator.of(context).pop(); + + final result = await Get.to( + () => DrawAnimationPage( + group: widget.group, + month: month, + year: year, + serverSeed: _serverSeed!, + nonce: _nonce!, + eligibleMembers: _eligibleMembers, + ), + transition: Transition.fadeIn, + duration: const Duration(milliseconds: 500), + ); + + // If draw was successful, reload draws in parent + if (result == true) { + final chitGroupService = Get.find(); + await chitGroupService.loadGroupMonthlyDraws(widget.group.id); + } } void _generateSeeds() { diff --git a/luckychit/lib/interfaces/manager/draw_animation_page.dart b/luckychit/lib/interfaces/manager/draw_animation_page.dart new file mode 100644 index 0000000..690378e --- /dev/null +++ b/luckychit/lib/interfaces/manager/draw_animation_page.dart @@ -0,0 +1,349 @@ +import 'package:flutter/material.dart'; +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 '../../shared/widgets/draw_animation_selector.dart'; + +class DrawAnimationPage extends StatefulWidget { + final ChitGroup group; + final int month; + final int year; + final String serverSeed; + final int nonce; + final List> eligibleMembers; + + const DrawAnimationPage({ + super.key, + required this.group, + required this.month, + required this.year, + required this.serverSeed, + required this.nonce, + required this.eligibleMembers, + }); + + @override + State createState() => _DrawAnimationPageState(); +} + +class _DrawAnimationPageState extends State + with SingleTickerProviderStateMixin { + late AnimationController _fadeController; + late Animation _fadeAnimation; + bool _isComplete = false; + + @override + void initState() { + super.initState(); + + _fadeController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _fadeController, + curve: Curves.easeInOut, + )); + + _fadeController.forward(); + } + + @override + void dispose() { + _fadeController.dispose(); + super.dispose(); + } + + void _onDrawComplete(String winnerId) async { + if (_isComplete) return; + + setState(() { + _isComplete = true; + }); + + // Wait a moment to show the winner + await Future.delayed(const Duration(seconds: 3)); + + // Save the draw result + final chitGroupService = Get.find(); + + try { + await chitGroupService.createMonthlyDraw( + widget.group.id, + widget.month, + widget.year, + clientSeed: 'DRAW_${DateTime.now().millisecondsSinceEpoch}', + ); + + // Navigate back with success + Get.back(result: true); + + Get.snackbar( + 'Draw Complete! 🎉', + 'Winner has been selected successfully', + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 3), + snackPosition: SnackPosition.TOP, + ); + } catch (e) { + 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), + ); + } + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + // Prevent back button during animation + if (_isComplete) return true; + + 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? This action cannot be undone.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Continue Draw'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + child: const Text('Cancel Draw'), + ), + ], + ), + ); + + return result ?? false; + }, + child: Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + child: FadeTransition( + opacity: _fadeAnimation, + child: Container( + 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, + ], + ), + ), + child: Column( + children: [ + // Header + Padding( + padding: EdgeInsets.all(20.w), + 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), + Expanded( + child: Column( + children: [ + Text( + widget.group.name, + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 8.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 6.h, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20.r), + border: Border.all( + color: Colors.white.withOpacity(0.3), + ), + ), + child: Text( + 'Month ${widget.month}/${widget.year}', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ], + ), + ), + SizedBox(width: 48.w), + ], + ), + ], + ), + ), + + // Animation Area + Expanded( + child: Center( + child: DrawAnimationSelector( + members: widget.eligibleMembers, + onDrawComplete: _onDrawComplete, + serverSeed: widget.serverSeed, + clientSeed: 'DRAW_${DateTime.now().millisecondsSinceEpoch}', + nonce: widget.nonce, + animationDuration: const Duration(seconds: 8), + ), + ), + ), + + // Footer + Padding( + padding: EdgeInsets.all(20.w), + 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), + ), + ), + child: Column( + 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, + ), + ], + ), + ], + ), + ), + SizedBox(height: 12.h), + Text( + '🎲 Provably Fair Draw', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withOpacity(0.7), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildInfoItem(String label, String value, IconData icon) { + return Column( + children: [ + Icon( + icon, + color: Colors.white.withOpacity(0.8), + size: 24.w, + ), + SizedBox(height: 6.h), + Text( + value, + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + Text( + label, + style: TextStyle( + fontSize: 12.sp, + color: Colors.white.withOpacity(0.7), + ), + ), + ], + ); + } +} + diff --git a/luckychit/lib/shared/widgets/alternative_draw_animations.dart b/luckychit/lib/shared/widgets/alternative_draw_animations.dart index ab439fb..c78b8d1 100644 --- a/luckychit/lib/shared/widgets/alternative_draw_animations.dart +++ b/luckychit/lib/shared/widgets/alternative_draw_animations.dart @@ -403,7 +403,6 @@ class _SlotMachineDrawAnimationState extends State String? _winnerId; List _displayNames = []; Timer? _slotTimer; - int _currentSelectedIndex = 0; @override void initState() { @@ -491,8 +490,6 @@ class _SlotMachineDrawAnimationState extends State setState(() { // Shuffle the display names for effect _displayNames.shuffle(); - // Update current selection pointer - _currentSelectedIndex = currentSpin % _displayNames.length; }); _slotController.forward().then((_) { @@ -516,7 +513,6 @@ class _SlotMachineDrawAnimationState extends State setState(() { _displayNames.shuffle(); - _currentSelectedIndex = currentSpin % _displayNames.length; }); _slotController.forward().then((_) { @@ -531,15 +527,27 @@ class _SlotMachineDrawAnimationState extends State } void _completeAnimation() { + final winnerName = widget.members.firstWhere((m) => m['id'] == _winnerId)['name']; + setState(() { _isAnimating = false; _isComplete = true; - // Set the winner's name at the top - _displayNames = [widget.members.firstWhere((m) => m['id'] == _winnerId)['name']]; + // Show winner in center position (index 2) with padding names + _displayNames = [ + _displayNames.isNotEmpty ? _displayNames[0] : '', + _displayNames.length > 1 ? _displayNames[1] : '', + winnerName, // Center - the winner! + _displayNames.length > 3 ? _displayNames[3] : '', + _displayNames.length > 4 ? _displayNames[4] : '', + ]; }); _pulseController.stop(); - widget.onDrawComplete(_winnerId!); + + // Delay callback slightly for visual effect + Future.delayed(const Duration(milliseconds: 500), () { + widget.onDrawComplete(_winnerId!); + }); } String _generateHash(String input) { @@ -640,55 +648,97 @@ class _SlotMachineDrawAnimationState extends State animation: _slotAnimation, builder: (context, child) { return Column( - children: _displayNames.take(5).map((name) { - final index = _displayNames.indexOf(name); - final isWinner = _isComplete && index == 0; - final isCurrentlySelected = _isAnimating && index == _currentSelectedIndex; + children: List.generate(5, (index) { + final displayIndex = index < _displayNames.length ? index : 0; + final name = _displayNames[displayIndex]; + final isWinner = _isComplete && index == 2; // Center position + final isCenterHighlight = _isAnimating && index == 2; // Always highlight center + return Expanded( child: AnimatedBuilder( animation: _pulseAnimation, builder: (context, child) { - return Transform.scale( - scale: isCurrentlySelected ? _pulseAnimation.value : 1.0, + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + transform: Matrix4.identity() + ..scale(isCenterHighlight || isWinner ? 1.05 : 1.0), child: Container( width: double.infinity, - margin: EdgeInsets.symmetric(vertical: 2.h), + margin: EdgeInsets.symmetric(vertical: 2.h, horizontal: 4.w), decoration: BoxDecoration( - color: isWinner - ? Colors.green.shade600 - : isCurrentlySelected - ? Colors.red.shade600 - : Colors.blue.shade600, - borderRadius: BorderRadius.circular(4.r), - boxShadow: isWinner ? [ - BoxShadow( - color: Colors.green.shade300, - blurRadius: 8.r, - spreadRadius: 2.r, - ), - ] : isCurrentlySelected ? [ - BoxShadow( - color: Colors.red.shade300, - blurRadius: 6.r, - spreadRadius: 1.r, - ), - ] : null, + gradient: isWinner + ? LinearGradient( + colors: [ + Colors.green.shade600, + Colors.green.shade700, + ], + ) + : isCenterHighlight + ? LinearGradient( + colors: [ + Colors.orange.shade600, + Colors.red.shade600, + ], + ) + : LinearGradient( + colors: [ + Colors.blue.shade700, + Colors.blue.shade800, + ], + ), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: isWinner || isCenterHighlight + ? Colors.white.withOpacity(0.6) + : Colors.white.withOpacity(0.1), + width: isWinner || isCenterHighlight ? 2.w : 1.w, + ), + boxShadow: isWinner + ? [ + BoxShadow( + color: Colors.green.shade300, + blurRadius: 12.r, + spreadRadius: 3.r, + ), + ] + : isCenterHighlight + ? [ + BoxShadow( + color: Colors.orange.shade300.withOpacity(0.6), + blurRadius: 8.r, + spreadRadius: 2.r, + ), + ] + : [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 4.r, + offset: Offset(0, 2.h), + ), + ], ), child: Center( child: Text( - name.length > 12 ? '${name.substring(0, 12)}...' : name, + name.length > 15 ? '${name.substring(0, 15)}...' : name, style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, + fontSize: isWinner || isCenterHighlight ? 18.sp : 15.sp, + fontWeight: isWinner || isCenterHighlight + ? FontWeight.w900 + : FontWeight.w600, color: Colors.white, - shadows: isWinner ? [ + letterSpacing: 0.5, + shadows: [ Shadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 2.r, + color: Colors.black.withOpacity(0.5), + blurRadius: 3.r, offset: Offset(1, 1), ), - ] : null, + ], ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), ), @@ -696,7 +746,7 @@ class _SlotMachineDrawAnimationState extends State }, ), ); - }).toList(), + }), ); }, ),