feat: ferris wheel page

This commit is contained in:
efrilm
2025-09-04 18:08:34 +07:00
parent 4d994e303b
commit 0d1e516686
6 changed files with 1334 additions and 168 deletions
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
class WheelSection {
final Color color;
final IconData icon;
final String prize;
final int value;
WheelSection({
required this.color,
required this.icon,
required this.prize,
required this.value,
});
}
class PrizeHistory {
final String prize;
final DateTime dateTime;
final int value;
final Color color;
final IconData icon;
PrizeHistory({
required this.prize,
required this.dateTime,
required this.value,
required this.color,
required this.icon,
});
}
@@ -0,0 +1,924 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;
import '../../../../common/theme/theme.dart';
import '../../../../common/painter/wheel_painter.dart';
import 'data/model.dart';
@RoutePage()
class FerrisWheelPage extends StatefulWidget {
const FerrisWheelPage({super.key});
@override
State<FerrisWheelPage> createState() => _FerrisWheelPageState();
}
class _FerrisWheelPageState extends State<FerrisWheelPage>
with TickerProviderStateMixin {
late AnimationController _rotationController;
late AnimationController _glowController;
late AnimationController _pulseController;
late AnimationController _idleRotationController;
Animation<double>? _spinAnimation;
Animation<double>? _pulseAnimation;
Animation<double>? _idleRotationAnimation;
int tokens = 3;
bool isSpinning = false;
String resultText = 'Belum pernah spin';
double currentRotation = 0.0;
int currentTabIndex = 0;
List<PrizeHistory> prizeHistory = [];
List<WheelSection> wheelSections = [
WheelSection(
color: AppColor.primary,
icon: Icons.visibility,
prize: '1 JT',
value: 1000000,
),
WheelSection(
color: AppColor.info,
icon: Icons.account_balance_wallet,
prize: '10RB',
value: 10000,
),
WheelSection(
color: AppColor.warning,
icon: Icons.card_giftcard,
prize: '50',
value: 50,
),
WheelSection(
color: AppColor.success,
icon: Icons.refresh,
prize: 'SPIN',
value: 1,
),
WheelSection(
color: AppColor.primaryDark,
icon: Icons.visibility,
prize: '30',
value: 30,
),
WheelSection(
color: AppColor.textLight,
icon: Icons.attach_money,
prize: '5RB',
value: 5000,
),
WheelSection(
color: AppColor.secondary,
icon: Icons.redeem,
prize: '20',
value: 20,
),
WheelSection(
color: AppColor.info,
icon: Icons.monetization_on,
prize: '5RB',
value: 5000,
),
];
@override
void initState() {
super.initState();
// Initialize animation controllers
_rotationController = AnimationController(
duration: const Duration(seconds: 4),
vsync: this,
);
_glowController = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
_pulseController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_idleRotationController = AnimationController(
duration: const Duration(seconds: 10), // 10 detik per putaran
vsync: this,
);
// Initialize animations
_pulseAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
);
_idleRotationAnimation = Tween<double>(
begin: 0.0,
end: 2 * math.pi,
).animate(_idleRotationController);
// Start animations
_glowController.repeat(reverse: true);
_pulseController.repeat(reverse: true);
_idleRotationController.repeat();
}
@override
void dispose() {
_rotationController.dispose();
_glowController.dispose();
_pulseController.dispose();
_idleRotationController.dispose();
super.dispose();
}
void _spinWheel() {
if (isSpinning || tokens <= 0) return;
setState(() {
isSpinning = true;
tokens--;
resultText = 'Sedang berputar...';
});
// Stop idle rotation
_idleRotationController.stop();
int targetSection = math.Random().nextInt(8);
double sectionAngle = (2 * math.pi) / 8;
double targetAngle = (targetSection * sectionAngle) + (sectionAngle / 2);
double baseRotations = 4 + math.Random().nextDouble() * 3;
// Calculate current total rotation
double currentIdleRotation = _idleRotationAnimation?.value ?? 0.0;
double finalRotation =
currentRotation +
currentIdleRotation +
(baseRotations * 2 * math.pi) +
targetAngle;
_spinAnimation =
Tween<double>(
begin: currentRotation + currentIdleRotation,
end: finalRotation,
).animate(
CurvedAnimation(
parent: _rotationController,
curve: Curves.easeOutCubic,
),
);
_rotationController.reset();
_rotationController.animateTo(1.0).then((_) {
setState(() {
currentRotation = finalRotation;
isSpinning = false;
resultText =
'Selamat! Anda mendapat ${wheelSections[targetSection].prize}!';
prizeHistory.insert(
0,
PrizeHistory(
prize: wheelSections[targetSection].prize,
dateTime: DateTime.now(),
value: wheelSections[targetSection].value,
color: wheelSections[targetSection].color,
icon: wheelSections[targetSection].icon,
),
);
});
_showWinDialog(wheelSections[targetSection]);
// Restart idle rotation
_idleRotationController.reset();
_idleRotationController.repeat();
});
}
void _showWinDialog(WheelSection section) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: AppColor.primaryGradient,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(40),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.2),
blurRadius: 15,
spreadRadius: 2,
),
],
),
child: Icon(section.icon, color: section.color, size: 40),
),
const SizedBox(height: 16),
Text(
'🎉 Selamat! 🎉',
style: AppStyle.h5.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textWhite,
),
),
const SizedBox(height: 8),
Text(
'Anda mendapatkan',
style: AppStyle.lg.copyWith(color: AppColor.textWhite),
),
const SizedBox(height: 4),
Text(
section.prize,
style: AppStyle.h5.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textWhite,
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.white,
foregroundColor: AppColor.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
padding: const EdgeInsets.symmetric(
horizontal: 30,
vertical: 12,
),
),
child: Text(
'Terima Kasih',
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
),
],
),
),
),
);
}
Widget _buildMainContent() {
return Column(
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColor.primary,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'G',
style: AppStyle.lg.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'GPY268e42',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Text(
'Warrior',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
],
),
],
),
Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: AppColor.warning,
shape: BoxShape.circle,
),
child: Icon(
Icons.circle,
size: 12,
color: AppColor.textWhite,
),
),
const SizedBox(width: 8),
Text(
'Token: $tokens',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
],
),
],
),
),
Text(
resultText,
style: AppStyle.md.copyWith(color: AppColor.textWhite),
),
const SizedBox(height: 20),
Expanded(
child: Center(
child: Stack(
alignment: Alignment.center,
children: [
AnimatedBuilder(
animation: _glowController,
builder: (context, child) {
return Container(
width: 340,
height: 340,
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppColor.white.withOpacity(
0.3 + 0.2 * _glowController.value,
),
blurRadius: 40,
spreadRadius: 10,
),
],
),
);
},
),
AnimatedBuilder(
animation: isSpinning
? _rotationController
: _idleRotationController,
builder: (context, child) {
double rotationAngle;
if (isSpinning) {
rotationAngle = _spinAnimation?.value ?? currentRotation;
} else {
rotationAngle =
currentRotation +
(_idleRotationAnimation?.value ?? 0.0);
}
return Transform.rotate(
angle: rotationAngle,
child: CustomPaint(
size: const Size(320, 320),
painter: WheelPainter(sections: wheelSections),
),
);
},
),
AnimatedBuilder(
animation:
_pulseAnimation ?? const AlwaysStoppedAnimation(1.0),
builder: (context, child) {
return Transform.scale(
scale: _pulseAnimation?.value ?? 1.0,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColor.warning, AppColor.warning],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 6),
),
BoxShadow(
color: AppColor.warning.withOpacity(0.5),
blurRadius: 20,
spreadRadius: (_pulseAnimation?.value ?? 1.0) * 5,
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(50),
onTap: _spinWheel,
child: Center(
child: Text(
'SPIN',
style: AppStyle.lg.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
),
),
),
),
);
},
),
Positioned(
top: 30,
child: Container(
width: 0,
height: 0,
decoration: BoxDecoration(
border: Border(
left: BorderSide(width: 15, color: Colors.transparent),
right: BorderSide(width: 15, color: Colors.transparent),
bottom: BorderSide(width: 30, color: AppColor.error),
),
),
),
),
],
),
),
),
Container(
margin: const EdgeInsets.all(20),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColor.warning, AppColor.warning],
),
borderRadius: BorderRadius.circular(15),
),
child: Text(
'❄️ Spin 30x lagi buat mainin spesial spin',
textAlign: TextAlign.center,
style: AppStyle.lg.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
),
),
),
),
],
);
}
Widget _buildPrizeListContent() {
return Container(
margin: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Text(
'🎁 Daftar Hadiah',
style: AppStyle.h6.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
),
const SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: wheelSections.length,
itemBuilder: (context, index) {
final section = wheelSections[index];
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: section.color,
borderRadius: BorderRadius.circular(25),
),
child: Icon(
section.icon,
color: AppColor.white,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
section.prize,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Text(
'Nilai: ${section.value}',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: section.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
),
child: Text(
'Hadiah',
style: AppStyle.sm.copyWith(
color: section.color,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
},
),
),
],
),
);
}
Widget _buildHistoryContent() {
return Container(
margin: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Text(
'📜 Riwayat Hadiah',
style: AppStyle.h6.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
),
const SizedBox(height: 20),
Expanded(
child: prizeHistory.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.history,
size: 80,
color: AppColor.white.withOpacity(0.5),
),
const SizedBox(height: 16),
Text(
'Belum ada riwayat spin',
style: AppStyle.lg.copyWith(
color: AppColor.white.withOpacity(0.7),
),
),
const SizedBox(height: 8),
Text(
'Mulai spin untuk melihat riwayat hadiah',
style: AppStyle.md.copyWith(
color: AppColor.white.withOpacity(0.5),
),
),
],
),
)
: ListView.builder(
itemCount: prizeHistory.length,
itemBuilder: (context, index) {
final history = prizeHistory[index];
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColor.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: history.color,
borderRadius: BorderRadius.circular(25),
),
child: Icon(
history.icon,
color: AppColor.white,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
history.prize,
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
Text(
'${history.dateTime.day}/${history.dateTime.month}/${history.dateTime.year} ${history.dateTime.hour.toString().padLeft(2, '0')}:${history.dateTime.minute.toString().padLeft(2, '0')}',
style: AppStyle.sm.copyWith(
color: AppColor.textSecondary,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: AppColor.success.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
),
child: Text(
'Menang',
style: AppStyle.sm.copyWith(
color: AppColor.success,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
},
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: AppColor.primaryGradient,
),
),
child: SafeArea(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
IconButton(
onPressed: () => context.router.back(),
icon: Icon(
Icons.close,
color: AppColor.textWhite,
size: 28,
),
),
Expanded(
child: Text(
'SPIN & WIN',
style: AppStyle.h6.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textWhite,
letterSpacing: 2,
),
),
),
IconButton(
onPressed: () {},
icon: Icon(Icons.volume_up, color: AppColor.textWhite),
),
IconButton(
onPressed: () {},
icon: Icon(Icons.info_outline, color: AppColor.textWhite),
),
],
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(25),
),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => setState(() => currentTabIndex = 0),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: currentTabIndex == 0
? AppColor.white
: Colors.transparent,
borderRadius: BorderRadius.circular(25),
),
child: Text(
'Spin Wheel',
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: currentTabIndex == 0
? AppColor.primary
: AppColor.textWhite,
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () => setState(() => currentTabIndex = 1),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: currentTabIndex == 1
? AppColor.white
: Colors.transparent,
borderRadius: BorderRadius.circular(25),
),
child: Text(
'Daftar Hadiah',
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: currentTabIndex == 1
? AppColor.primary
: AppColor.textWhite,
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () => setState(() => currentTabIndex = 2),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: currentTabIndex == 2
? AppColor.white
: Colors.transparent,
borderRadius: BorderRadius.circular(25),
),
child: Text(
'Riwayat',
textAlign: TextAlign.center,
style: AppStyle.md.copyWith(
fontWeight: FontWeight.bold,
color: currentTabIndex == 2
? AppColor.primary
: AppColor.textWhite,
),
),
),
),
),
],
),
),
const SizedBox(height: 20),
Expanded(
child: currentTabIndex == 0
? _buildMainContent()
: currentTabIndex == 1
? _buildPrizeListContent()
: _buildHistoryContent(),
),
],
),
),
),
);
}
}