fixed slot machine
This commit is contained in:
parent
004328489d
commit
a56c3e0922
|
|
@ -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<CombinedDrawDialog>
|
|||
}
|
||||
}
|
||||
|
||||
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<ChitGroupService>();
|
||||
await chitGroupService.loadGroupMonthlyDraws(widget.group.id);
|
||||
}
|
||||
}
|
||||
|
||||
void _generateSeeds() {
|
||||
|
|
|
|||
|
|
@ -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<Map<String, dynamic>> 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<DrawAnimationPage> createState() => _DrawAnimationPageState();
|
||||
}
|
||||
|
||||
class _DrawAnimationPageState extends State<DrawAnimationPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _fadeController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
bool _isComplete = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_fadeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(
|
||||
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<ChitGroupService>();
|
||||
|
||||
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<bool>(
|
||||
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<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),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -403,7 +403,6 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|||
String? _winnerId;
|
||||
List<String> _displayNames = [];
|
||||
Timer? _slotTimer;
|
||||
int _currentSelectedIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -491,8 +490,6 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|||
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<SlotMachineDrawAnimation>
|
|||
|
||||
setState(() {
|
||||
_displayNames.shuffle();
|
||||
_currentSelectedIndex = currentSpin % _displayNames.length;
|
||||
});
|
||||
|
||||
_slotController.forward().then((_) {
|
||||
|
|
@ -531,15 +527,27 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|||
}
|
||||
|
||||
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<SlotMachineDrawAnimation>
|
|||
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<SlotMachineDrawAnimation>
|
|||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in New Issue