866 lines
29 KiB
Dart
866 lines
29 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.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 '../../core/themes/draw_slot_theme.dart';
|
|
import '../../shared/widgets/draw_animation_selector.dart';
|
|
import '../../core/utils/whatsapp_util.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;
|
|
/// 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();
|
|
_animationClientSeed = 'DRAW_${DateTime.now().millisecondsSinceEpoch}';
|
|
|
|
_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;
|
|
});
|
|
|
|
// Let the slot settle on the winner before the confirmation sheet
|
|
await Future.delayed(const Duration(milliseconds: 900));
|
|
|
|
// Find winner (avoid firstWhere/orElse RTI mismatch Map<String,String> vs dynamic on web)
|
|
Map<String, dynamic> winner = {'name': 'Unknown', 'mobile': ''};
|
|
for (final raw in widget.eligibleMembers) {
|
|
final m = Map<String, dynamic>.from(raw);
|
|
if ('${m['id']}' == '$winnerId') {
|
|
winner = m;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Get the bid amount for this month from financial data
|
|
final chitGroupService = Get.find<ChitGroupService>();
|
|
await chitGroupService.loadGroupFinancialData(widget.group.id);
|
|
|
|
// Find the financial data for this specific month
|
|
final monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
final monthKey = '${monthNames[widget.month - 1]}-${widget.year.toString().substring(2)}';
|
|
|
|
final financialEntry = chitGroupService.financialData.firstWhereOrNull(
|
|
(entry) => entry.monthYear == monthKey,
|
|
);
|
|
|
|
final bidAmount = financialEntry?.bidAmount ?? widget.group.monthlyInstallment;
|
|
|
|
// Show confirmation dialog with winner and bid amount
|
|
final shouldSave = await _showWinnerConfirmation(winner, bidAmount);
|
|
|
|
if (shouldSave == true) {
|
|
await _saveDrawResult(winnerId, bidAmount);
|
|
} else {
|
|
_exitDrawScreen(recorded: false);
|
|
}
|
|
}
|
|
|
|
Future<bool?> _showWinnerConfirmation(Map<String, dynamic> winner, double bidAmount) async {
|
|
await HapticFeedback.mediumImpact();
|
|
final monthNames = [
|
|
'January', 'February', 'March', 'April', 'May', 'June',
|
|
'July', 'August', 'September', 'October', 'November', 'December',
|
|
];
|
|
final periodLabel =
|
|
'${monthNames[widget.month - 1]} ${widget.year} · ${widget.group.name}';
|
|
|
|
return showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (dialogContext) => TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0.88, end: 1.0),
|
|
duration: const Duration(milliseconds: 420),
|
|
curve: Curves.easeOutCubic,
|
|
builder: (context, scale, child) =>
|
|
Transform.scale(scale: scale, child: child),
|
|
child: AlertDialog(
|
|
insetPadding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 24.h),
|
|
backgroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20.r),
|
|
),
|
|
title: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(10.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.amber.shade50,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.emoji_events_rounded,
|
|
color: Colors.amber.shade800,
|
|
size: 28.w,
|
|
),
|
|
),
|
|
SizedBox(width: 12.w),
|
|
Expanded(
|
|
child: Text(
|
|
'Winner',
|
|
style: TextStyle(
|
|
fontSize: 22.sp,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.grey.shade900,
|
|
letterSpacing: -0.5,
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
tooltip: 'Close',
|
|
onPressed: () => Navigator.pop(dialogContext, false),
|
|
icon: Icon(Icons.close, color: Colors.grey.shade700),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Text(
|
|
periodLabel,
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey.shade600,
|
|
height: 1.3,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: double.infinity,
|
|
padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 22.h),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
Colors.green.shade50,
|
|
Colors.teal.shade50.withOpacity(0.85),
|
|
],
|
|
),
|
|
borderRadius: BorderRadius.circular(16.r),
|
|
border: Border.all(color: Colors.green.shade200, width: 1.5),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
winner['name']?.toString() ?? 'Unknown',
|
|
style: TextStyle(
|
|
fontSize: 26.sp,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.green.shade900,
|
|
height: 1.15,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
if ((winner['mobile']?.toString() ?? '').isNotEmpty) ...[
|
|
SizedBox(height: 10.h),
|
|
Text(
|
|
winner['mobile']?.toString() ?? '',
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
color: Colors.grey.shade700,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
SizedBox(height: 18.h),
|
|
Container(
|
|
height: 1,
|
|
color: Colors.green.shade200.withOpacity(0.9),
|
|
),
|
|
SizedBox(height: 14.h),
|
|
Text(
|
|
'Prize amount',
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
color: Colors.grey.shade600,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
SizedBox(height: 6.h),
|
|
Text(
|
|
_formatIndianCurrency(bidAmount),
|
|
style: TextStyle(
|
|
fontSize: 30.sp,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.green.shade800,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 16.h),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => _shareViaWhatsApp(winner, bidAmount),
|
|
icon: Icon(Icons.send_rounded,
|
|
size: 18.w, color: const Color(0xFF25D366)),
|
|
label: Text(
|
|
'Share',
|
|
style: TextStyle(
|
|
fontSize: 13.sp, color: const Color(0xFF25D366)),
|
|
),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(vertical: 12.h),
|
|
side: const BorderSide(color: Color(0xFF25D366)),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {
|
|
Get.snackbar(
|
|
'Screenshot',
|
|
'Capture this screen if you want a local copy.',
|
|
backgroundColor: Colors.blue.shade700,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
},
|
|
icon: Icon(Icons.photo_camera_outlined,
|
|
size: 18.w, color: Colors.blue.shade700),
|
|
label: Text(
|
|
'Capture',
|
|
style: TextStyle(
|
|
fontSize: 13.sp, color: Colors.blue.shade700),
|
|
),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(vertical: 12.h),
|
|
side: BorderSide(color: Colors.blue.shade700),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 12.h),
|
|
Text(
|
|
'Save to record this result and get a shareable public link.',
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
color: Colors.grey.shade700,
|
|
fontWeight: FontWeight.w500,
|
|
height: 1.35,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actionsPadding: EdgeInsets.fromLTRB(20.w, 0, 20.w, 16.h),
|
|
actions: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: () => Navigator.pop(dialogContext, false),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(vertical: 14.h),
|
|
side: BorderSide(color: Colors.grey.shade400),
|
|
),
|
|
child: Text(
|
|
'Discard',
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade800,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 10.w),
|
|
Expanded(
|
|
flex: 2,
|
|
child: FilledButton.icon(
|
|
onPressed: () => Navigator.pop(dialogContext, true),
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: Colors.green.shade700,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(vertical: 14.h),
|
|
),
|
|
icon: Icon(Icons.check_circle_outline, size: 20.w),
|
|
label: Text(
|
|
'Save result',
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _shareViaWhatsApp(Map<String, dynamic> winner, double bidAmount) {
|
|
final monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
final monthName = monthNames[widget.month - 1];
|
|
|
|
final message = '''
|
|
🎉 *${widget.group.name}* 🎉
|
|
|
|
📅 *Monthly Draw Result*
|
|
Month: $monthName ${widget.year}
|
|
|
|
🏆 *WINNER*
|
|
${winner['name']}
|
|
${winner['mobile']}
|
|
|
|
💰 *Prize Amount*
|
|
${_formatIndianCurrency(bidAmount)}
|
|
|
|
✨ This draw was conducted using our provably fair system for complete transparency.
|
|
|
|
_Congratulations to the winner!_
|
|
''';
|
|
|
|
// Share to all members or specific number
|
|
WhatsAppUtil.shareText(message);
|
|
}
|
|
|
|
Future<void> _saveDrawResult(String winnerId, double bidAmount) async {
|
|
final chitGroupService = Get.find<ChitGroupService>();
|
|
|
|
try {
|
|
// Show loading
|
|
Get.dialog(
|
|
PopScope(
|
|
canPop: false,
|
|
child: Center(
|
|
child: Container(
|
|
padding: EdgeInsets.all(32.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16.r),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
CircularProgressIndicator(),
|
|
SizedBox(height: 16.h),
|
|
Text(
|
|
'Saving draw result...',
|
|
style: TextStyle(fontSize: 16.sp),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
barrierDismissible: false,
|
|
);
|
|
|
|
final created = await chitGroupService.createMonthlyDraw(
|
|
widget.group.id,
|
|
widget.month,
|
|
widget.year,
|
|
clientSeed: _animationClientSeed,
|
|
prizeAmount: bidAmount,
|
|
animationWinnerUserId: winnerId,
|
|
serverSeed: widget.serverSeed,
|
|
nonce: widget.nonce,
|
|
);
|
|
|
|
if (!mounted) return;
|
|
Get.back();
|
|
|
|
if (created == null) {
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to save draw result',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 4),
|
|
snackPosition: SnackPosition.TOP,
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_drawRecorded = true;
|
|
});
|
|
|
|
final drawId = created['id']?.toString();
|
|
if (drawId != null) {
|
|
final publicUrl = await WhatsAppUtil.getDrawPublicShareUrl(drawId);
|
|
if (mounted && publicUrl != null && publicUrl.isNotEmpty) {
|
|
await _showPublicResultLinkDialog(publicUrl);
|
|
} else if (mounted && (publicUrl == null || publicUrl.isEmpty)) {
|
|
Get.snackbar(
|
|
'Draw saved',
|
|
'Public link unavailable — set PUBLIC_BASE_URL on the server to share results.',
|
|
backgroundColor: Colors.orange.shade700,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 5),
|
|
snackPosition: SnackPosition.TOP,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!mounted) return;
|
|
_exitDrawScreen(recorded: true);
|
|
|
|
Get.snackbar(
|
|
'Draw Saved! 🎉',
|
|
'Winner has been recorded. Share the public link so anyone can view results.',
|
|
backgroundColor: Colors.green,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 3),
|
|
snackPosition: SnackPosition.TOP,
|
|
);
|
|
} catch (e) {
|
|
if (mounted) {
|
|
try {
|
|
Get.back();
|
|
} catch (_) {}
|
|
}
|
|
|
|
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: true,
|
|
builder: (ctx) => AlertDialog(
|
|
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: () async {
|
|
await Clipboard.setData(ClipboardData(text: url));
|
|
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 only'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.of(ctx).pop(),
|
|
child: const Text('Done'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _formatIndianCurrency(double amount) {
|
|
int intAmount = amount.round();
|
|
String amountStr = intAmount.toString();
|
|
String formatted = '';
|
|
|
|
if (amountStr.length <= 3) {
|
|
formatted = amountStr;
|
|
} else {
|
|
int remaining = amountStr.length;
|
|
int start = 0;
|
|
|
|
if (remaining > 3) {
|
|
formatted = amountStr.substring(amountStr.length - 3);
|
|
remaining -= 3;
|
|
start = amountStr.length - 3;
|
|
} else {
|
|
formatted = amountStr;
|
|
remaining = 0;
|
|
}
|
|
|
|
while (remaining > 0) {
|
|
int groupSize = remaining >= 2 ? 2 : remaining;
|
|
int groupStart = start - groupSize;
|
|
String group = amountStr.substring(groupStart, start);
|
|
formatted = group + ',' + formatted;
|
|
start = groupStart;
|
|
remaining -= groupSize;
|
|
}
|
|
}
|
|
|
|
return '₹$formatted';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
const monthLabels = [
|
|
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
|
|
];
|
|
final drawPeriodShort =
|
|
'${monthLabels[widget.month - 1]} ${widget.year}';
|
|
final theme = Theme.of(context);
|
|
final d = DrawSlotTheming(theme.colorScheme, theme.brightness);
|
|
|
|
return PopScope(
|
|
canPop: false,
|
|
onPopInvokedWithResult: (didPop, result) async {
|
|
if (didPop || !mounted) return;
|
|
await _handleLeaveIntent();
|
|
},
|
|
child: Scaffold(
|
|
backgroundColor: Colors.transparent,
|
|
body: SafeArea(
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
decoration: BoxDecoration(
|
|
gradient: d.pageBackdrop,
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Header
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(8.w, 12.h, 12.w, 8.h),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
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: theme.textTheme.headlineSmall?.copyWith(
|
|
color: d.pageOnText,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: -0.3,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
SizedBox(height: 10.h),
|
|
Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 14.w,
|
|
vertical: 8.h,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: d.chipFill,
|
|
borderRadius: BorderRadius.circular(20.r),
|
|
border: Border.all(color: d.chipBorder),
|
|
),
|
|
child: Text(
|
|
drawPeriodShort,
|
|
style: theme.textTheme.labelLarge?.copyWith(
|
|
color: d.pageOnText,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.2,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(width: 48.w),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Animation Area - Scrollable
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
|
child: DrawAnimationSelector(
|
|
members: widget.eligibleMembers,
|
|
onDrawComplete: _onDrawComplete,
|
|
serverSeed: widget.serverSeed,
|
|
clientSeed: _animationClientSeed,
|
|
nonce: widget.nonce,
|
|
animationDuration: const Duration(seconds: 8),
|
|
allowReplay: !_isComplete,
|
|
),
|
|
),
|
|
),
|
|
|
|
// Footer
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 16.h),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 12.w,
|
|
vertical: 14.h,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: d.chipFill,
|
|
borderRadius: BorderRadius.circular(18.r),
|
|
border: Border.all(color: d.chipBorder),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
_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: 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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoItem(
|
|
DrawSlotTheming d,
|
|
String label,
|
|
String value,
|
|
IconData icon,
|
|
) {
|
|
return Column(
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
color: d.pageMuted,
|
|
size: 22.w,
|
|
),
|
|
SizedBox(height: 6.h),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: 20.sp,
|
|
fontWeight: FontWeight.w800,
|
|
color: d.pageOnText,
|
|
),
|
|
),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 11.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: d.pageMuted,
|
|
letterSpacing: 0.2,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|