fixed animations
This commit is contained in:
parent
5c284a3698
commit
1c861e1f4b
|
|
@ -0,0 +1,187 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import 'app_theme.dart';
|
||||
|
||||
/// Visual tokens for the full-screen monthly draw / slot machine.
|
||||
/// Anchored to [AppTheme] teal seed + amber tertiary; respects light/dark [ColorScheme].
|
||||
class DrawSlotTheming {
|
||||
DrawSlotTheming(this.scheme, this.brightness);
|
||||
|
||||
final ColorScheme scheme;
|
||||
final Brightness brightness;
|
||||
|
||||
bool get _isDark => brightness == Brightness.dark;
|
||||
|
||||
/// Immersive page background (draw route).
|
||||
LinearGradient get pageBackdrop {
|
||||
if (_isDark) {
|
||||
final deep = Color.alphaBlend(
|
||||
scheme.primary.withOpacity(0.5),
|
||||
const Color(0xFF041014),
|
||||
);
|
||||
final mid = Color.alphaBlend(
|
||||
scheme.primary.withOpacity(0.22),
|
||||
const Color(0xFF0A1C22),
|
||||
);
|
||||
return LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
stops: const [0.0, 0.55, 1.0],
|
||||
colors: [
|
||||
deep,
|
||||
mid,
|
||||
Color.alphaBlend(scheme.surface.withOpacity(0.15), const Color(0xFF020608)),
|
||||
],
|
||||
);
|
||||
}
|
||||
return LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color.alphaBlend(scheme.primary.withOpacity(0.12), scheme.surface),
|
||||
scheme.surface,
|
||||
Color.alphaBlend(scheme.primaryContainer.withOpacity(0.35), scheme.surface),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color get pageOnText =>
|
||||
_isDark ? Colors.white.withOpacity(0.95) : scheme.onSurface;
|
||||
|
||||
Color get pageMuted =>
|
||||
_isDark ? Colors.white.withOpacity(0.72) : scheme.onSurfaceVariant;
|
||||
|
||||
Color get chipFill => _isDark
|
||||
? Colors.white.withOpacity(0.12)
|
||||
: scheme.primaryContainer.withOpacity(0.45);
|
||||
|
||||
Color get chipBorder =>
|
||||
_isDark ? Colors.white.withOpacity(0.22) : scheme.outlineVariant;
|
||||
|
||||
/// Pre-start intro card (on draw page).
|
||||
BoxDecoration introCardDecoration() {
|
||||
return BoxDecoration(
|
||||
color: _isDark
|
||||
? Color.alphaBlend(Colors.white.withOpacity(0.08), const Color(0xFF0C181C))
|
||||
: scheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
border: Border.all(
|
||||
color: _isDark
|
||||
? Colors.white.withOpacity(0.14)
|
||||
: scheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: scheme.primary.withOpacity(_isDark ? 0.18 : 0.08),
|
||||
blurRadius: 24.r,
|
||||
offset: Offset(0, 12.h),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Slot machine outer chassis.
|
||||
BoxDecoration slotChassisDecoration() {
|
||||
return BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: _isDark
|
||||
? [
|
||||
Color.alphaBlend(scheme.surfaceContainerHigh.withOpacity(0.4), const Color(0xFF0D1619)),
|
||||
const Color(0xFF050A0C),
|
||||
]
|
||||
: [
|
||||
scheme.surfaceContainerHigh,
|
||||
scheme.surfaceContainer,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(22.r),
|
||||
border: Border.all(
|
||||
color: Color.alphaBlend(
|
||||
scheme.tertiary.withOpacity(0.85),
|
||||
scheme.outline,
|
||||
),
|
||||
width: 2.w,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: scheme.primary.withOpacity(_isDark ? 0.35 : 0.12),
|
||||
blurRadius: 28.r,
|
||||
spreadRadius: -2,
|
||||
offset: Offset(0, 14.h),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(_isDark ? 0.45 : 0.06),
|
||||
blurRadius: 20.r,
|
||||
offset: Offset(0, 8.h),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color get slotInnerWell => _isDark
|
||||
? const Color(0xFF020608)
|
||||
: Color.alphaBlend(scheme.surfaceContainerHighest, scheme.surface);
|
||||
|
||||
List<Color> rowColorsIdle() => _isDark
|
||||
? [
|
||||
Color.alphaBlend(scheme.surfaceContainerHigh.withOpacity(0.5), const Color(0xFF1A2529)),
|
||||
Color.alphaBlend(scheme.primary.withOpacity(0.35), const Color(0xFF0F181C)),
|
||||
]
|
||||
: [
|
||||
scheme.surfaceContainerHigh,
|
||||
scheme.surfaceContainer,
|
||||
];
|
||||
|
||||
List<Color> rowColorsSpinning() => [
|
||||
Color.alphaBlend(scheme.primary.withOpacity(0.55), rowColorsIdle().first),
|
||||
Color.alphaBlend(scheme.primary.withOpacity(0.4), rowColorsIdle().last),
|
||||
];
|
||||
|
||||
List<Color> rowColorsWinner() => _isDark
|
||||
? [
|
||||
Color.alphaBlend(scheme.primary.withOpacity(0.9), const Color(0xFF0A3030)),
|
||||
Color.alphaBlend(scheme.tertiary.withOpacity(0.35), scheme.primary),
|
||||
]
|
||||
: [
|
||||
scheme.primary,
|
||||
Color.alphaBlend(scheme.tertiary.withOpacity(0.28), scheme.primary),
|
||||
];
|
||||
|
||||
Color get winnerRowText =>
|
||||
_isDark ? Colors.white : scheme.onPrimary;
|
||||
|
||||
Color get idleRowText => _isDark
|
||||
? Colors.white.withOpacity(0.92)
|
||||
: scheme.onSurface;
|
||||
|
||||
Color get winnerFrameColor =>
|
||||
Color.alphaBlend(scheme.tertiary.withOpacity(0.95), AppTheme.accent);
|
||||
|
||||
BoxDecoration winnerSummaryDecoration() {
|
||||
return BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color.alphaBlend(scheme.primaryContainer.withOpacity(_isDark ? 0.35 : 0.65), scheme.surface),
|
||||
Color.alphaBlend(scheme.tertiary.withOpacity(_isDark ? 0.12 : 0.2), scheme.surface),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(18.r),
|
||||
border: Border.all(
|
||||
color: scheme.primary.withOpacity(0.35),
|
||||
width: 1.5,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle winnerSummaryTitle(TextTheme t) => (t.titleLarge ?? const TextStyle()).copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: scheme.primary,
|
||||
height: 1.2,
|
||||
);
|
||||
|
||||
Color get winnerSummaryIcon => scheme.tertiary;
|
||||
}
|
||||
|
|
@ -258,6 +258,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return RecordingOverlay(
|
||||
showRecordingIndicator: true,
|
||||
child: Dialog(
|
||||
|
|
@ -272,7 +273,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
Container(
|
||||
padding: EdgeInsets.all(16.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple.shade600,
|
||||
color: scheme.primary,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16.r),
|
||||
topRight: Radius.circular(16.r),
|
||||
|
|
@ -281,8 +282,8 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.casino,
|
||||
color: Colors.white,
|
||||
Icons.emoji_events_rounded,
|
||||
color: scheme.onPrimary,
|
||||
size: 24.w,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
|
|
@ -293,7 +294,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
Text(
|
||||
'Monthly Draw',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: scheme.onPrimary,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
|
@ -301,7 +302,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
Text(
|
||||
'${_monthController.text}/${_yearController.text}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
color: scheme.onPrimary.withOpacity(0.9),
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
|
|
@ -310,7 +311,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
icon: Icon(Icons.close, color: scheme.onPrimary),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
|
|
@ -342,9 +343,12 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
Container(
|
||||
padding: EdgeInsets.all(16.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.grey.shade200, width: 1),
|
||||
top: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16.r),
|
||||
|
|
@ -374,11 +378,11 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _startDraw,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.purple.shade600,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: scheme.primary,
|
||||
foregroundColor: scheme.onPrimary,
|
||||
padding: EdgeInsets.symmetric(vertical: 14.h),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
|
|
@ -406,14 +410,14 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
),
|
||||
if (_isDrawComplete)
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
child: FilledButton(
|
||||
onPressed: _saveDrawResult,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green.shade600,
|
||||
foregroundColor: Colors.white,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: scheme.primary,
|
||||
foregroundColor: scheme.onPrimary,
|
||||
padding: EdgeInsets.symmetric(vertical: 14.h),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
|
|
@ -440,6 +444,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
}
|
||||
|
||||
Widget _buildDrawForm() {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
|
|
@ -451,7 +456,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
|
@ -555,14 +560,16 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
height: 16.w,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.purple.shade600),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(scheme.primary),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h),
|
||||
decoration: BoxDecoration(
|
||||
color: _eligibleMembers.isEmpty ? Colors.orange.shade100 : Colors.purple.shade100,
|
||||
color: _eligibleMembers.isEmpty
|
||||
? Colors.orange.shade100
|
||||
: scheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Text(
|
||||
|
|
@ -570,7 +577,9 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _eligibleMembers.isEmpty ? Colors.orange.shade800 : Colors.purple.shade800,
|
||||
color: _eligibleMembers.isEmpty
|
||||
? Colors.orange.shade800
|
||||
: scheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -637,12 +646,12 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
return ListTile(
|
||||
dense: true,
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.purple.shade100,
|
||||
backgroundColor: scheme.primaryContainer,
|
||||
radius: 18.r,
|
||||
child: Text(
|
||||
(member['name']?.isNotEmpty == true ? member['name'].substring(0, 1) : 'M').toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: Colors.purple.shade700,
|
||||
color: scheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
|
|
@ -660,8 +669,8 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
style: TextStyle(fontSize: 12.sp),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green.shade600,
|
||||
Icons.check_circle_rounded,
|
||||
color: scheme.primary,
|
||||
size: 18.w,
|
||||
),
|
||||
);
|
||||
|
|
@ -679,21 +688,22 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
}
|
||||
|
||||
Widget _buildDrawResult() {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
children: [
|
||||
// Winner Announcement
|
||||
Container(
|
||||
padding: EdgeInsets.all(20.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
border: Border.all(color: Colors.green.shade200),
|
||||
color: scheme.primaryContainer.withOpacity(0.65),
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
border: Border.all(color: scheme.primary.withOpacity(0.25)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.emoji_events,
|
||||
color: Colors.amber.shade600,
|
||||
Icons.emoji_events_rounded,
|
||||
color: scheme.tertiary,
|
||||
size: 48.w,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
|
@ -702,7 +712,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green.shade800,
|
||||
color: scheme.primary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
|
|
@ -711,7 +721,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.green.shade700,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
|
|
@ -719,7 +729,7 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
_winnerData?['mobile'] ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.grey.shade600,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -731,20 +741,21 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
Container(
|
||||
padding: EdgeInsets.all(12.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(color: Colors.blue.shade200),
|
||||
color: scheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
border: Border.all(color: scheme.outlineVariant),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.security, color: Colors.blue.shade600, size: 16.w),
|
||||
Icon(Icons.verified_user_rounded, color: scheme.primary, size: 18.w),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Draw completed using provably fair system. Recording saved.',
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.blue.shade700,
|
||||
color: scheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -771,11 +782,12 @@ class _CombinedDrawDialogState extends State<CombinedDrawDialog>
|
|||
|
||||
if (created != null) {
|
||||
Navigator.of(context).pop();
|
||||
final s = Theme.of(context).colorScheme;
|
||||
Get.snackbar(
|
||||
'Success',
|
||||
'Monthly draw completed successfully!',
|
||||
backgroundColor: Colors.green.shade600,
|
||||
colorText: Colors.white,
|
||||
backgroundColor: s.primary,
|
||||
colorText: s.onPrimary,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:get/get.dart';
|
|||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import '../../core/services/chit_group_service.dart';
|
||||
import '../../core/models/chit_group.dart';
|
||||
import '../../core/themes/draw_slot_theme.dart';
|
||||
import '../../shared/widgets/draw_animation_selector.dart';
|
||||
import '../../core/utils/whatsapp_util.dart';
|
||||
|
||||
|
|
@ -34,9 +35,81 @@ class _DrawAnimationPageState extends State<DrawAnimationPage>
|
|||
late AnimationController _fadeController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
bool _isComplete = false;
|
||||
/// True after the draw is successfully persisted (safe to leave without prompt).
|
||||
bool _drawRecorded = false;
|
||||
/// Single client seed for the whole page so animation + API save use the same value.
|
||||
late final String _animationClientSeed;
|
||||
|
||||
void _exitDrawScreen({required bool recorded}) {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(recorded);
|
||||
}
|
||||
|
||||
/// System back / header — always offers a way out.
|
||||
Future<void> _handleLeaveIntent() async {
|
||||
if (!_isComplete) {
|
||||
final cancel = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Cancel draw?'),
|
||||
content: const Text(
|
||||
'The draw is still in progress. Stop and go back?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Keep drawing'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Stop'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (cancel == true && mounted) {
|
||||
_exitDrawScreen(recorded: false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_drawRecorded) {
|
||||
_exitDrawScreen(recorded: true);
|
||||
return;
|
||||
}
|
||||
|
||||
final leave = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Leave without saving?'),
|
||||
content: const Text(
|
||||
'This winner is not recorded yet. If you leave now, you will need to run the draw again.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Stay'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Leave anyway'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (leave == true && mounted) {
|
||||
_exitDrawScreen(recorded: false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -104,8 +177,7 @@ class _DrawAnimationPageState extends State<DrawAnimationPage>
|
|||
if (shouldSave == true) {
|
||||
await _saveDrawResult(winnerId, bidAmount);
|
||||
} else {
|
||||
// User cancelled, go back without saving
|
||||
Get.back(result: false);
|
||||
_exitDrawScreen(recorded: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,6 +234,11 @@ class _DrawAnimationPageState extends State<DrawAnimationPage>
|
|||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Close',
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
icon: Icon(Icons.close, color: Colors.grey.shade700),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
|
|
@ -429,7 +506,6 @@ _Congratulations to the winner!_
|
|||
Get.back();
|
||||
|
||||
if (created == null) {
|
||||
Get.back(result: false);
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Failed to save draw result',
|
||||
|
|
@ -441,6 +517,10 @@ _Congratulations to the winner!_
|
|||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_drawRecorded = true;
|
||||
});
|
||||
|
||||
final drawId = created['id']?.toString();
|
||||
if (drawId != null) {
|
||||
final publicUrl = await WhatsAppUtil.getDrawPublicShareUrl(drawId);
|
||||
|
|
@ -459,7 +539,7 @@ _Congratulations to the winner!_
|
|||
}
|
||||
|
||||
if (!mounted) return;
|
||||
Get.back(result: true);
|
||||
_exitDrawScreen(recorded: true);
|
||||
|
||||
Get.snackbar(
|
||||
'Draw Saved! 🎉',
|
||||
|
|
@ -470,10 +550,13 @@ _Congratulations to the winner!_
|
|||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) Get.back();
|
||||
|
||||
Get.back(result: false);
|
||||
if (mounted) {
|
||||
try {
|
||||
Get.back();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Failed to save draw result: ${e.toString()}',
|
||||
|
|
@ -483,34 +566,47 @@ _Congratulations to the winner!_
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showPublicResultLinkDialog(String url) async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierDismissible: true,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Public result link'),
|
||||
content: SelectableText(
|
||||
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();
|
||||
if (ctx.mounted) {
|
||||
Get.snackbar(
|
||||
'Copied',
|
||||
'Link copied — anyone can open it in a browser',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Copy link'),
|
||||
child: const Text('Copy only'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('Done'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -558,40 +654,17 @@ _Congratulations to the winner!_
|
|||
];
|
||||
final drawPeriodShort =
|
||||
'${monthLabels[widget.month - 1]} ${widget.year}';
|
||||
final theme = Theme.of(context);
|
||||
final d = DrawSlotTheming(theme.colorScheme, theme.brightness);
|
||||
|
||||
return PopScope(
|
||||
canPop: _isComplete,
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
if (didPop || !mounted) return;
|
||||
final leave = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Leave draw?'),
|
||||
content: const Text(
|
||||
'The result is not saved yet. Leave anyway?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Stay'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Leave'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (leave == true && mounted) {
|
||||
Get.back(result: false);
|
||||
}
|
||||
await _handleLeaveIntent();
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
backgroundColor: Colors.transparent,
|
||||
body: SafeArea(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
|
|
@ -599,93 +672,63 @@ _Congratulations to the winner!_
|
|||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.purple.shade900,
|
||||
Colors.blue.shade900,
|
||||
Colors.indigo.shade900,
|
||||
],
|
||||
),
|
||||
gradient: d.pageBackdrop,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: EdgeInsets.all(20.w),
|
||||
padding: EdgeInsets.fromLTRB(8.w, 12.h, 12.w, 8.h),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (!_isComplete)
|
||||
IconButton(
|
||||
tooltip: _drawRecorded
|
||||
? 'Done'
|
||||
: (_isComplete
|
||||
? 'Close'
|
||||
: 'Cancel draw'),
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
_drawRecorded
|
||||
? Icons.check_circle_outline_rounded
|
||||
: Icons.close_rounded,
|
||||
color: d.pageOnText,
|
||||
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'),
|
||||
onPressed: () => _handleLeaveIntent(),
|
||||
),
|
||||
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,
|
||||
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,54 +760,63 @@ _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),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12.w,
|
||||
vertical: 14.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
color: d.chipFill,
|
||||
borderRadius: BorderRadius.circular(18.r),
|
||||
border: Border.all(color: d.chipBorder),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
'Eligible Members',
|
||||
d,
|
||||
'In this draw',
|
||||
'${widget.eligibleMembers.length}',
|
||||
Icons.people,
|
||||
Icons.how_to_reg_rounded,
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 30.h,
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
height: 36.h,
|
||||
color: d.chipBorder.withOpacity(0.6),
|
||||
),
|
||||
_buildInfoItem(
|
||||
'Total Members',
|
||||
d,
|
||||
'Group size',
|
||||
'${widget.group.maxMembers}',
|
||||
Icons.group,
|
||||
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(height: 12.h),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
'🎲 Provably Fair Draw',
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
fontWeight: FontWeight.w500,
|
||||
'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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|||
import 'dart:math' as math;
|
||||
import 'dart:async';
|
||||
|
||||
import '../../core/themes/draw_slot_theme.dart';
|
||||
|
||||
// Base class for all draw animations
|
||||
abstract class DrawAnimation extends StatefulWidget {
|
||||
final List<Map<String, dynamic>> members;
|
||||
|
|
@ -632,6 +634,10 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final d = DrawSlotTheming(scheme, theme.brightness);
|
||||
|
||||
String? winnerName;
|
||||
if (_winnerId != null && _winnerId!.isNotEmpty) {
|
||||
for (final m in widget.members) {
|
||||
|
|
@ -644,57 +650,79 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|||
|
||||
final maxW = MediaQuery.sizeOf(context).width - 40.w;
|
||||
|
||||
return Center(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: d.slotChassisDecoration(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(14.w),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.grey.shade900,
|
||||
Colors.grey.shade800,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
color: d.slotInnerWell,
|
||||
borderRadius: BorderRadius.circular(14.r),
|
||||
border: Border.all(
|
||||
color: scheme.outline.withOpacity(0.28),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: EdgeInsets.all(18.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
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;
|
||||
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;
|
||||
|
||||
|
|
@ -702,34 +730,28 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|||
child: AnimatedBuilder(
|
||||
animation: _pulseAnimation,
|
||||
builder: (context, child) {
|
||||
final double scale =
|
||||
isWinner || isCenterHighlight
|
||||
? 1.08
|
||||
final scale = isWinner ||
|
||||
isCenterHighlight
|
||||
? 1.05
|
||||
: 1.0;
|
||||
final double fontSize = isWinner
|
||||
? 24.sp
|
||||
List<Color> rowColors;
|
||||
if (isWinner) {
|
||||
rowColors = d.rowColorsWinner();
|
||||
} else if (isCenterHighlight) {
|
||||
rowColors =
|
||||
d.rowColorsSpinning();
|
||||
} else {
|
||||
rowColors = d.rowColorsIdle();
|
||||
}
|
||||
|
||||
final borderColor = isWinner
|
||||
? scheme.tertiary
|
||||
.withOpacity(0.9)
|
||||
: isCenterHighlight
|
||||
? 20.sp
|
||||
: 18.sp;
|
||||
final FontWeight weight = isWinner
|
||||
? FontWeight.w900
|
||||
: isCenterHighlight
|
||||
? FontWeight.w800
|
||||
: FontWeight.w700;
|
||||
final List<Color> colors = isWinner
|
||||
? [
|
||||
Colors.green.shade500,
|
||||
Colors.green.shade600,
|
||||
]
|
||||
: isCenterHighlight
|
||||
? [
|
||||
Colors.deepPurple.shade500,
|
||||
Colors.deepPurple.shade700,
|
||||
]
|
||||
: [
|
||||
Colors.blueGrey.shade700,
|
||||
Colors.blueGrey.shade900,
|
||||
];
|
||||
? scheme.primary
|
||||
.withOpacity(0.55)
|
||||
: scheme.outline
|
||||
.withOpacity(0.22);
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(
|
||||
|
|
@ -739,37 +761,39 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|||
..scale(scale),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: EdgeInsets.symmetric(
|
||||
vertical: 6.h,
|
||||
horizontal: 12.w,
|
||||
margin:
|
||||
EdgeInsets.symmetric(
|
||||
vertical: 4.h,
|
||||
horizontal: 10.w,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: colors,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
gradient:
|
||||
LinearGradient(
|
||||
colors: rowColors,
|
||||
begin: Alignment
|
||||
.centerLeft,
|
||||
end: Alignment
|
||||
.centerRight,
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(10.r),
|
||||
BorderRadius
|
||||
.circular(12.r),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(
|
||||
isWinner ||
|
||||
isCenterHighlight
|
||||
? 0.7
|
||||
: 0.15),
|
||||
width: isWinner ||
|
||||
isCenterHighlight
|
||||
color: borderColor,
|
||||
width: isWinner
|
||||
? 2.w
|
||||
: 1.w,
|
||||
),
|
||||
boxShadow: [
|
||||
if (isWinner)
|
||||
BoxShadow(
|
||||
color: Colors.black
|
||||
.withOpacity(0.4),
|
||||
blurRadius: isWinner
|
||||
? 14.r
|
||||
: 6.r,
|
||||
offset: Offset(0, 3.h),
|
||||
color: scheme
|
||||
.primary
|
||||
.withOpacity(
|
||||
0.45),
|
||||
blurRadius: 16.r,
|
||||
offset: Offset(
|
||||
0, 4.h),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -778,25 +802,49 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|||
name.length > 22
|
||||
? '${name.substring(0, 22)}…'
|
||||
: name,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: weight,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.6,
|
||||
shadows: [
|
||||
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.5),
|
||||
blurRadius: 4.r,
|
||||
color: Colors
|
||||
.black
|
||||
.withOpacity(
|
||||
0.45),
|
||||
blurRadius:
|
||||
3,
|
||||
offset:
|
||||
const Offset(1.5, 1.5),
|
||||
const Offset(
|
||||
0,
|
||||
1),
|
||||
),
|
||||
],
|
||||
]
|
||||
: null,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
textAlign:
|
||||
TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
overflow: TextOverflow
|
||||
.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -808,6 +856,40 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|||
);
|
||||
},
|
||||
),
|
||||
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,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
|
|
@ -817,15 +899,17 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
borderRadius:
|
||||
BorderRadius.circular(12.r),
|
||||
border: Border.all(
|
||||
color: Colors.amber.shade400,
|
||||
color: d.winnerFrameColor,
|
||||
width: 2.5.w,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.amber.withOpacity(0.35),
|
||||
blurRadius: 14.r,
|
||||
color: scheme.tertiary
|
||||
.withOpacity(0.35),
|
||||
blurRadius: 18.r,
|
||||
spreadRadius: 0.5,
|
||||
),
|
||||
],
|
||||
|
|
@ -840,35 +924,29 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 14.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),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16.w, vertical: 16.h),
|
||||
decoration: d.winnerSummaryDecoration(),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.emoji_events, color: Colors.amber.shade700, size: 26.w),
|
||||
SizedBox(width: 10.w),
|
||||
Icon(
|
||||
Icons.emoji_events_rounded,
|
||||
color: d.winnerSummaryIcon,
|
||||
size: 28.w,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
winnerName,
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.green.shade900,
|
||||
height: 1.2,
|
||||
),
|
||||
style: d.winnerSummaryTitle(theme.textTheme)
|
||||
.copyWith(fontSize: 18.sp),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
|
@ -881,6 +959,7 @@ class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,97 +48,101 @@ class _DrawAnimationSelectorState extends State<DrawAnimationSelector> {
|
|||
});
|
||||
}
|
||||
|
||||
Widget _buildAnimationSelector() {
|
||||
Widget _buildAnimationSelector(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final d = DrawSlotTheming(scheme, theme.brightness);
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(24.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10.r,
|
||||
offset: Offset(0, 5.h),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(22.w, 26.h, 22.w, 24.h),
|
||||
decoration: d.introCardDecoration(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 72.w,
|
||||
height: 72.w,
|
||||
width: 56.w,
|
||||
height: 56.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple.shade50,
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
color: scheme.primaryContainer.withOpacity(
|
||||
theme.brightness == Brightness.dark ? 0.35 : 0.65),
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.casino,
|
||||
color: Colors.purple.shade600,
|
||||
size: 36.w,
|
||||
Icons.auto_awesome_rounded,
|
||||
color: scheme.primary,
|
||||
size: 28.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),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.play_arrow, size: 20.w),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
'Start Slot Machine',
|
||||
style: TextStyle(fontSize: 16.sp),
|
||||
'Monthly draw',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.2,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
'Fair random pick with a clear on-screen reveal.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
height: 1.35,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.h),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainerHighest.withOpacity(
|
||||
theme.brightness == Brightness.dark ? 0.5 : 1),
|
||||
borderRadius: BorderRadius.circular(14.r),
|
||||
border: Border.all(color: scheme.outlineVariant),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.people_alt_rounded,
|
||||
size: 20.w,
|
||||
color: scheme.primary,
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${widget.members.length} eligible members in this draw',
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 22.h),
|
||||
FilledButton.icon(
|
||||
onPressed: _startDraw,
|
||||
icon: Icon(Icons.play_arrow_rounded, size: 22.w),
|
||||
label: Text(
|
||||
'Start draw',
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -153,7 +158,7 @@ class _DrawAnimationSelectorState extends State<DrawAnimationSelector> {
|
|||
width: maxW,
|
||||
child: Column(
|
||||
children: [
|
||||
if (!_isDrawStarted) _buildAnimationSelector(),
|
||||
if (!_isDrawStarted) _buildAnimationSelector(context),
|
||||
if (_isDrawStarted) ...[
|
||||
SlotMachineDrawAnimation(
|
||||
members: widget.members,
|
||||
|
|
@ -167,29 +172,21 @@ class _DrawAnimationSelectorState extends State<DrawAnimationSelector> {
|
|||
if (widget.allowReplay)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _resetDraw,
|
||||
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: 12.h),
|
||||
padding: EdgeInsets.symmetric(vertical: 14.h),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
borderRadius: BorderRadius.circular(16.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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in New Issue