diff --git a/assets/icons/cup.svg b/assets/icons/cup.svg
new file mode 100644
index 0000000..7f3dff8
--- /dev/null
+++ b/assets/icons/cup.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/icons/info-circle.svg b/assets/icons/info-circle.svg
new file mode 100644
index 0000000..c828515
--- /dev/null
+++ b/assets/icons/info-circle.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/location-tick.svg b/assets/icons/location-tick.svg
new file mode 100644
index 0000000..4b5b969
--- /dev/null
+++ b/assets/icons/location-tick.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/medal-star.svg b/assets/icons/medal-star.svg
new file mode 100644
index 0000000..de8f27b
--- /dev/null
+++ b/assets/icons/medal-star.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/radar-2.svg b/assets/icons/radar-2.svg
new file mode 100644
index 0000000..b3c46e3
--- /dev/null
+++ b/assets/icons/radar-2.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/ranking.svg b/assets/icons/ranking.svg
new file mode 100644
index 0000000..c334a86
--- /dev/null
+++ b/assets/icons/ranking.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/search-normal.svg b/assets/icons/search-normal.svg
new file mode 100644
index 0000000..81b836e
--- /dev/null
+++ b/assets/icons/search-normal.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/timer_Hunt.svg b/assets/icons/timer_Hunt.svg
new file mode 100644
index 0000000..18a8f26
--- /dev/null
+++ b/assets/icons/timer_Hunt.svg
@@ -0,0 +1,3 @@
+
diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart
index 05059b7..eabf92f 100644
--- a/lib/gen/assets.gen.dart
+++ b/lib/gen/assets.gen.dart
@@ -135,6 +135,9 @@ class $AssetsIconsGen {
/// File path: assets/icons/coin.svg
SvgGenImage get coin => const SvgGenImage('assets/icons/coin.svg');
+ /// File path: assets/icons/cup.svg
+ SvgGenImage get cup => const SvgGenImage('assets/icons/cup.svg');
+
/// File path: assets/icons/current loc.svg
SvgGenImage get currentLoc =>
const SvgGenImage('assets/icons/current loc.svg');
@@ -227,6 +230,10 @@ class $AssetsIconsGen {
SvgGenImage get icRoundLocalOffer =>
const SvgGenImage('assets/icons/ic_round-local-offer.svg');
+ /// File path: assets/icons/info-circle.svg
+ SvgGenImage get infoCircle =>
+ const SvgGenImage('assets/icons/info-circle.svg');
+
/// File path: assets/icons/infoPic.svg
SvgGenImage get infoPic => const SvgGenImage('assets/icons/infoPic.svg');
@@ -251,6 +258,10 @@ class $AssetsIconsGen {
/// File path: assets/icons/list.svg
SvgGenImage get list => const SvgGenImage('assets/icons/list.svg');
+ /// File path: assets/icons/location-tick.svg
+ SvgGenImage get locationTick =>
+ const SvgGenImage('assets/icons/location-tick.svg');
+
/// File path: assets/icons/location.svg
SvgGenImage get location => const SvgGenImage('assets/icons/location.svg');
@@ -296,6 +307,9 @@ class $AssetsIconsGen {
SvgGenImage get materialSymbolsLocationWork =>
const SvgGenImage('assets/icons/material-symbols_location-work.svg');
+ /// File path: assets/icons/medal-star.svg
+ SvgGenImage get medalStar => const SvgGenImage('assets/icons/medal-star.svg');
+
/// File path: assets/icons/message-question.svg
SvgGenImage get messageQuestion =>
const SvgGenImage('assets/icons/message-question.svg');
@@ -344,6 +358,12 @@ class $AssetsIconsGen {
/// File path: assets/icons/profile.svg
SvgGenImage get profile => const SvgGenImage('assets/icons/profile.svg');
+ /// File path: assets/icons/radar-2.svg
+ SvgGenImage get radar2 => const SvgGenImage('assets/icons/radar-2.svg');
+
+ /// File path: assets/icons/ranking.svg
+ SvgGenImage get ranking => const SvgGenImage('assets/icons/ranking.svg');
+
/// File path: assets/icons/receipt-discount 2.svg
SvgGenImage get receiptDiscount2 =>
const SvgGenImage('assets/icons/receipt-discount 2.svg');
@@ -365,6 +385,10 @@ class $AssetsIconsGen {
/// File path: assets/icons/routing.svg
SvgGenImage get routing => const SvgGenImage('assets/icons/routing.svg');
+ /// File path: assets/icons/search-normal.svg
+ SvgGenImage get searchNormal =>
+ const SvgGenImage('assets/icons/search-normal.svg');
+
/// File path: assets/icons/selected list.svg
SvgGenImage get selectedList =>
const SvgGenImage('assets/icons/selected list.svg');
@@ -427,6 +451,9 @@ class $AssetsIconsGen {
/// File path: assets/icons/timer.svg
SvgGenImage get timer => const SvgGenImage('assets/icons/timer.svg');
+ /// File path: assets/icons/timer_Hunt.svg
+ SvgGenImage get timerHunt => const SvgGenImage('assets/icons/timer_Hunt.svg');
+
/// File path: assets/icons/usa circle.svg
SvgGenImage get usaCircle => const SvgGenImage('assets/icons/usa circle.svg');
@@ -467,6 +494,7 @@ class $AssetsIconsGen {
clander,
clock,
coin,
+ cup,
currentLoc,
deliveryOff,
deliveryOn,
@@ -492,6 +520,7 @@ class $AssetsIconsGen {
hugeiconsBabyBoyDress,
hugeiconsCheeseCake01,
icRoundLocalOffer,
+ infoCircle,
infoPic,
ionFastFoodOutline,
iranCircle,
@@ -499,6 +528,7 @@ class $AssetsIconsGen {
like,
link2,
list,
+ locationTick,
location,
locationPopup,
logout,
@@ -511,6 +541,7 @@ class $AssetsIconsGen {
materialSymbolsLocationOn,
materialSymbolsLocationOnn,
materialSymbolsLocationWork,
+ medalStar,
messageQuestion,
microphone2,
nearby,
@@ -525,12 +556,15 @@ class $AssetsIconsGen {
pickupOn,
profile2,
profile,
+ radar2,
+ ranking,
receiptDiscount2,
receiptDiscount,
recenter,
riSearch2Line,
routing2,
routing,
+ searchNormal,
selectedList,
shield,
shoppingCart,
@@ -549,6 +583,7 @@ class $AssetsIconsGen {
timerPause,
timerStart,
timer,
+ timerHunt,
usaCircle,
];
}
diff --git a/lib/screens/mains/hunt/ENHANCED_LEADERBOARD_README.md b/lib/screens/mains/hunt/ENHANCED_LEADERBOARD_README.md
new file mode 100644
index 0000000..a6ca04a
--- /dev/null
+++ b/lib/screens/mains/hunt/ENHANCED_LEADERBOARD_README.md
@@ -0,0 +1,208 @@
+# 🏆 Hunt Leaderboard - Enhanced UI Documentation
+
+## Overview
+The Hunt Leaderboard has been completely redesigned with a modern, engaging, and visually attractive interface that provides an exceptional user experience for the Rank Hunt feature.
+
+## 🎨 Key Visual Enhancements
+
+### 1. **Modern Design System**
+- **Gradient Backgrounds**: Beautiful gradient overlays with depth and dimension
+- **Enhanced Shadows**: Multi-layered shadows with proper opacity and spread
+- **Rounded Corners**: Consistent 16-32px border radius for modern feel
+- **Improved Typography**: Better font weights, sizes, and letter spacing
+
+### 2. **Advanced Animations**
+- **Elastic Entry Animations**: Items bounce in with elastic curves
+- **Particle Background Effect**: Animated floating particles for dynamic background
+- **Trophy Glow Effects**: Pulsing glow animations for top 3 positions
+- **Smooth Transitions**: 600-1200ms duration with proper easing curves
+
+### 3. **Enhanced Visual Hierarchy**
+
+#### **Header Section**
+- Gradient icon container with shadow effects
+- Improved title typography with better spacing
+- Enhanced close button with card-style background
+
+#### **Statistics Overview**
+- Interactive stats cards showing:
+ - Total number of hunters
+ - Total points accumulated
+ - User's current rank
+- Color-coded icons and gradient backgrounds
+- Professional dividers between stats
+
+#### **Champion Podium (Top 3)**
+- **3D Podium Effect**: Different heights for 1st, 2nd, 3rd place
+- **Dynamic Trophy System**:
+ - Gold (1st): Animated glow with golden gradient
+ - Silver (2nd): Silver gradient with medium glow
+ - Bronze (3rd): Bronze gradient with subtle glow
+- **Real-time Rank Colors**: Dynamic color assignment based on performance
+- **Avatar Integration**: Circular avatars with border effects
+
+### 4. **Participant List Enhancements**
+- **Smart Categorization**: Top 3 in podium, others in scrollable list
+- **Current User Highlighting**: Special gradient background and borders
+- **Achievement Badges**: Fire icons for high performers (500+ points)
+- **Enhanced Card Design**: Gradient backgrounds with professional shadows
+- **Interactive Elements**: Hover effects and touch responses
+
+## 🎯 Technical Features
+
+### **Performance Optimizations**
+- **Efficient Animations**: Multiple animation controllers with proper disposal
+- **Smooth Scrolling**: Optimized ListView with proper item building
+- **Memory Management**: Proper controller lifecycle management
+
+### **Responsive Design**
+- **Adaptive Heights**: 85% screen height for optimal viewing
+- **Flexible Layouts**: Responsive to different screen sizes
+- **Safe Area Handling**: Proper padding and margins
+
+### **Accessibility Features**
+- **High Contrast**: Proper color contrast ratios
+- **Readable Typography**: Appropriate font sizes and weights
+- **Touch Targets**: Proper button sizes for easy interaction
+
+## 🎪 Additional Components Created
+
+### 1. **CelebrationOverlay** (`celebration_overlay.dart`)
+- **Confetti Animation**: 30 animated particles with different shapes
+- **Burst Effects**: Expanding circle effects from center
+- **Color Variety**: 8 different celebration colors
+- **Star Particles**: Mixed circle and star-shaped confetti
+- **Fade Out Effect**: Smooth opacity transitions
+
+### 2. **AnimatedRankBadge** (`animated_rank_badge.dart`)
+- **Pulse Animation**: Breathing effect for active badges
+- **Shimmer Effect**: Metallic shine for top 3 ranks
+- **Dynamic Colors**: Rank-based color schemes
+- **Icon System**: Different icons for different rank levels
+- **Point Display**: Integrated point counter with star icon
+
+## 🎨 Color Scheme & Theming
+
+### **Rank-Based Colors**
+- **1st Place**: Gold (#FFD700) with warm gradients
+- **2nd Place**: Silver (#C0C0C0) with cool gradients
+- **3rd Place**: Bronze (#CD7F32) with earth tones
+- **Top 10**: Primary blue with professional gradients
+- **Others**: Subtle gray tones with accent highlights
+
+### **Background Effects**
+- **Primary Gradient**: Surface to card background transition
+- **Particle Colors**: Primary color with 10% opacity
+- **Shadow System**: Multi-layered shadows with proper falloff
+
+## 🚀 Usage Examples
+
+### **Basic Implementation**
+```dart
+LeaderboardWidget(
+ entries: leaderboardEntries,
+ userPoints: currentUserPoints,
+ onClose: () => Navigator.pop(context),
+)
+```
+
+### **With Celebration**
+```dart
+CelebrationOverlay(
+ showCelebration: userAchievedNewRank,
+ onCelebrationComplete: () => _showLeaderboard(),
+ child: YourHuntScreen(),
+)
+```
+
+### **Rank Badge Usage**
+```dart
+AnimatedRankBadge(
+ rank: userRank,
+ totalPoints: userPoints,
+ userName: currentUser.name,
+ showAnimation: true,
+ onTap: () => _showFullLeaderboard(),
+)
+```
+
+## 🎯 Benefits of Enhanced UI
+
+### **User Engagement**
+- **Visual Appeal**: Modern gradients and animations attract attention
+- **Gamification**: Trophy systems and celebrations motivate participation
+- **Recognition**: Clear visual hierarchy emphasizes achievements
+
+### **User Experience**
+- **Intuitive Navigation**: Clear close buttons and smooth transitions
+- **Information Density**: Optimal balance of data and visual elements
+- **Performance**: Smooth 60fps animations with proper optimization
+
+### **Competitive Spirit**
+- **Clear Rankings**: Easy to understand position and progress
+- **Achievement Visibility**: Badges and effects highlight success
+- **Social Comparison**: Easy comparison with other participants
+
+## 🎪 Animation Details
+
+### **Entry Sequence**
+1. **Slide Up**: Main container slides from bottom (600ms)
+2. **Statistics Fade**: Stats cards fade in with stagger
+3. **Podium Rise**: Top 3 podium rises with elastic curve
+4. **List Items**: Participants animate in with 80ms intervals
+5. **Background**: Particles start floating immediately
+
+### **Interactive Feedback**
+- **Button Hover**: Subtle scale and shadow changes
+- **Card Tap**: Brief scale down with shadow adjustment
+- **Trophy Glow**: Continuous pulse for top 3 positions
+
+## 📱 Mobile Optimization
+
+### **Touch Interactions**
+- **Swipe to Close**: Gesture-based closing mechanism
+- **Large Touch Targets**: Minimum 44px touch areas
+- **Haptic Feedback**: Proper vibration responses
+
+### **Performance**
+- **Efficient Rendering**: Only necessary repaints
+- **Memory Management**: Proper animation disposal
+- **Battery Optimization**: Controlled animation refresh rates
+
+## 🚀 Latest Updates & Fixes
+
+### **Navigation & Layout Optimizations**
+- **Fixed Navigation Overflow**: Content no longer falls under navigation bar
+- **Dynamic Height Calculation**: Adapts to different screen sizes and safe areas
+- **Compact Design**: Optimized spacing for better content density
+- **Mobile-First**: Responsive design that works perfectly on all devices
+
+### **Podium Enhancement**
+- **Fixed Name Truncation**: Smart name shortening with first name + last initial
+- **Improved Text Layout**: Better spacing prevents overflow issues
+- **Interactive Podium**: Tap to see full participant details
+- **Optimized Heights**: Adjusted podium heights for better proportion
+
+### **Advanced Features**
+- **Smart Search**: Quick search functionality for large participant lists
+- **Live Counter Animation**: Animated participant count in statistics
+- **Pull-to-Refresh**: Swipe down to refresh leaderboard data
+- **Real-time Indicators**: Live status indicators throughout the UI
+
+### **Performance Optimizations**
+- **Fixed Item Heights**: Improved scrolling performance
+- **Efficient Filtering**: Smart filtering for search functionality
+- **Memory Management**: Proper animation controller disposal
+- **Smooth Animations**: 60fps animations with optimized refresh rates
+
+This enhanced Hunt Leaderboard transforms a simple ranking display into an engaging, beautiful, and highly interactive experience that motivates users to participate more actively in the Hunt challenges! 🎉
+
+### **Usage with New Features**
+```dart
+LeaderboardWidget(
+ entries: leaderboardEntries,
+ userPoints: currentUserPoints,
+ onClose: () => Navigator.pop(context),
+ onRefresh: () => _refreshLeaderboardData(), // New refresh callback
+)
+```
diff --git a/lib/screens/mains/hunt/examples/hunt_usage_examples.dart b/lib/screens/mains/hunt/examples/hunt_usage_examples.dart
index 654ca88..dd20efc 100644
--- a/lib/screens/mains/hunt/examples/hunt_usage_examples.dart
+++ b/lib/screens/mains/hunt/examples/hunt_usage_examples.dart
@@ -1,10 +1,3 @@
-/*
- * Hunt Feature Usage Examples
- *
- * This file demonstrates how to use the Hunt feature components
- * and integrate them into your app.
- */
-
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:lba/screens/mains/hunt/hunt.dart';
@@ -13,7 +6,6 @@ import 'package:lba/screens/mains/hunt/models/hunt_card.dart';
import 'package:lba/screens/mains/hunt/widgets/hunt_card_widget.dart';
import 'package:lba/screens/mains/hunt/widgets/leaderboard_widget.dart';
-// Example 1: Basic Hunt Screen Integration
class ExampleHuntIntegration extends StatelessWidget {
const ExampleHuntIntegration({super.key});
@@ -26,7 +18,6 @@ class ExampleHuntIntegration extends StatelessWidget {
}
}
-// Example 2: Custom Hunt Card Widget Usage
class ExampleHuntCard extends StatelessWidget {
const ExampleHuntCard({super.key});
@@ -52,7 +43,6 @@ class ExampleHuntCard extends StatelessWidget {
child: HuntCardWidget(
card: exampleCard,
onTap: () {
- // Handle card selection
print('Card ${exampleCard.id} selected!');
},
isSelected: false,
@@ -63,7 +53,6 @@ class ExampleHuntCard extends StatelessWidget {
}
}
-// Example 3: Leaderboard Widget Usage
class ExampleLeaderboard extends StatelessWidget {
const ExampleLeaderboard({super.key});
@@ -106,7 +95,6 @@ class ExampleLeaderboard extends StatelessWidget {
}
}
-// Example 4: Custom Hunt State Management
class ExampleHuntStateUsage extends StatefulWidget {
const ExampleHuntStateUsage({super.key});
@@ -140,12 +128,10 @@ class _ExampleHuntStateUsageState extends State {
builder: (context, state, child) {
return Column(
children: [
- // Display current game state
Text('Current State: ${state.gameState}'),
Text('User Points: ${state.userPoints}'),
Text('Cards Available: ${state.cards.length}'),
- // Control buttons
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
@@ -170,7 +156,6 @@ class _ExampleHuntStateUsageState extends State {
],
),
- // Display selected card info
if (state.selectedCard != null) ...[
const SizedBox(height: 20),
Text('Selected Card: ${state.selectedCard!.category}'),
@@ -178,7 +163,6 @@ class _ExampleHuntStateUsageState extends State {
Text('Question: ${state.selectedCard!.question}'),
],
- // Time remaining display
if (state.gameState == HuntGameState.huntingActive) ...[
const SizedBox(height: 20),
Text('Time Remaining: ${_formatDuration(state.timeRemaining)}'),
@@ -201,7 +185,6 @@ class _ExampleHuntStateUsageState extends State {
}
}
-// Example 5: Custom Hunt Integration with Navigation
class ExampleHuntNavigation extends StatelessWidget {
const ExampleHuntNavigation({super.key});
@@ -253,15 +236,4 @@ class ExampleHuntNavigation extends StatelessWidget {
),
);
}
-}
-
-/*
- * Integration Notes:
- *
- * 1. Always wrap Hunt components with ChangeNotifierProvider
- * 2. Initialize the game state with huntState.initializeGame()
- * 3. Handle permissions properly before starting hunts
- * 4. Clean up resources in dispose methods
- * 5. Use Consumer widgets to react to state changes
- * 6. Test on physical devices for location and camera features
- */
+}
\ No newline at end of file
diff --git a/lib/screens/mains/hunt/hunt.dart b/lib/screens/mains/hunt/hunt.dart
index 1a0aa9b..b042bcd 100644
--- a/lib/screens/mains/hunt/hunt.dart
+++ b/lib/screens/mains/hunt/hunt.dart
@@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:lba/gen/assets.gen.dart';
import 'package:provider/provider.dart';
import 'package:vibration/vibration.dart';
import 'package:lba/res/colors.dart';
@@ -95,7 +97,6 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
}
void _initializeGame() {
- // Initialization is handled in provider creation
}
@override
@@ -278,7 +279,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
decoration: BoxDecoration(
color: AppColors.isDarkMode
? Colors.white.withOpacity(0.05)
- : Colors.white.withOpacity(0.4), // Liquid Glass effect
+ : Colors.white.withOpacity(0.4),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.isDarkMode
@@ -306,12 +307,11 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
: const Color(0xFF1976D2).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
- child: Icon(
- Icons.search_rounded,
+ child: SvgPicture.asset(
+ Assets.icons.searchNormal.path,
color: AppColors.isDarkMode
? Colors.white
: const Color(0xFF1976D2),
- size: 20,
),
),
const SizedBox(width: 12),
@@ -357,12 +357,11 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
- Icon(
- Icons.leaderboard_rounded,
+ SvgPicture.asset(
+ Assets.icons.ranking.path,
color: AppColors.isDarkMode
? Colors.white
: const Color(0xFF1976D2),
- size: 16,
),
const SizedBox(width: 6),
Text(
@@ -371,7 +370,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
color: AppColors.isDarkMode
? Colors.white
: const Color(0xFF1976D2),
- fontSize: 12,
+ fontSize: 14,
fontWeight: FontWeight.w600,
),
),
@@ -393,7 +392,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
decoration: BoxDecoration(
color: AppColors.isDarkMode
? Colors.white.withOpacity(0.10)
- : Colors.white.withOpacity(0.6), // More transparent
+ : Colors.white.withOpacity(0.6),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.isDarkMode
@@ -413,7 +412,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(
- icon: Icons.stars_rounded,
+ icon: Assets.icons.medalStar.path,
label: 'Total Points',
value: '${huntProvider.userPoints}',
color: AppColors.isDarkMode
@@ -436,7 +435,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
),
),
_buildStatItem(
- icon: Icons.emoji_events,
+ icon: Assets.icons.cup.path,
label: 'Rank',
value: '#${huntProvider.currentUserRank}',
color: AppColors.isDarkMode
@@ -449,7 +448,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
}
Widget _buildStatItem({
- required IconData icon,
+ required String icon,
required String label,
required String value,
required Color color,
@@ -463,7 +462,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
- child: Icon(icon, color: color, size: 24),
+ child: SvgPicture.asset(icon, color: color,),
),
const SizedBox(height: 8),
Text(
@@ -494,7 +493,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
decoration: BoxDecoration(
color: AppColors.isDarkMode
? Colors.white.withOpacity(0.08)
- : Colors.white.withOpacity(0.7), // More transparent
+ : Colors.white.withOpacity(0.7),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.isDarkMode
diff --git a/lib/screens/mains/hunt/providers/hunt_provider.dart b/lib/screens/mains/hunt/providers/hunt_provider.dart
index e4581be..9eac090 100644
--- a/lib/screens/mains/hunt/providers/hunt_provider.dart
+++ b/lib/screens/mains/hunt/providers/hunt_provider.dart
@@ -19,7 +19,6 @@ class HuntState extends ChangeNotifier {
bool _isCameraPermissionGranted = false;
DateTime? _huntStartTime;
- // Getters
List get cards => _cards;
HuntCard? get selectedCard => _selectedCard;
HuntGameState get gameState => _gameState;
@@ -60,7 +59,6 @@ class HuntState extends ChangeNotifier {
void selectCard(HuntCard card) {
_selectedCard = card;
- // Don't change game state, keep it as cardSelection so cards just flip
notifyListeners();
}
@@ -105,7 +103,6 @@ class HuntState extends ChangeNotifier {
}
void _updateLeaderboard() {
- // Update user's position in leaderboard
for (int i = 0; i < _leaderboard.length; i++) {
if (_leaderboard[i].isCurrentUser) {
_leaderboard[i] = _leaderboard[i].copyWith(totalPoints: _userPoints);
@@ -113,10 +110,8 @@ class HuntState extends ChangeNotifier {
}
}
- // Sort leaderboard by points
_leaderboard.sort((a, b) => b.totalPoints.compareTo(a.totalPoints));
- // Update ranks
for (int i = 0; i < _leaderboard.length; i++) {
_leaderboard[i] = _leaderboard[i].copyWith(rank: i + 1);
}
diff --git a/lib/screens/mains/hunt/services/location_service.dart b/lib/screens/mains/hunt/services/location_service.dart
index f7b8d94..5e56517 100644
--- a/lib/screens/mains/hunt/services/location_service.dart
+++ b/lib/screens/mains/hunt/services/location_service.dart
@@ -8,8 +8,8 @@ class LocationService {
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
- return permission == LocationPermission.whileInUse ||
- permission == LocationPermission.always;
+ return permission == LocationPermission.whileInUse ||
+ permission == LocationPermission.always;
}
static Future checkCameraPermission() async {
@@ -34,24 +34,35 @@ class LocationService {
}
static double calculateDistance(
- double lat1, double lon1,
- double lat2, double lon2,
+ double lat1,
+ double lon1,
+ double lat2,
+ double lon2,
) {
return Geolocator.distanceBetween(lat1, lon1, lat2, lon2);
}
static bool isWithinRange(
- double currentLat, double currentLon,
- double targetLat, double targetLon,
- {double rangeInMeters = 50.0}
- ) {
- double distance = calculateDistance(currentLat, currentLon, targetLat, targetLon);
+ double currentLat,
+ double currentLon,
+ double targetLat,
+ double targetLon, {
+ double rangeInMeters = 50.0,
+ }) {
+ double distance = calculateDistance(
+ currentLat,
+ currentLon,
+ targetLat,
+ targetLon,
+ );
return distance <= rangeInMeters;
}
static double getBearing(
- double startLat, double startLng,
- double endLat, double endLng,
+ double startLat,
+ double startLng,
+ double endLat,
+ double endLng,
) {
final startLatRad = startLat * (math.pi / 180);
final startLngRad = startLng * (math.pi / 180);
@@ -61,10 +72,11 @@ class LocationService {
double dLng = endLngRad - startLngRad;
double y = math.sin(dLng) * math.cos(endLatRad);
- double x = math.cos(startLatRad) * math.sin(endLatRad) -
+ double x =
+ math.cos(startLatRad) * math.sin(endLatRad) -
math.sin(startLatRad) * math.cos(endLatRad) * math.cos(dLng);
double bearing = math.atan2(y, x);
- return (bearing * (180 / math.pi) + 360) % 360; // Convert to degrees
+ return (bearing * (180 / math.pi) + 360) % 360;
}
}
diff --git a/lib/screens/mains/hunt/test/hunt_test.dart b/lib/screens/mains/hunt/test/hunt_test.dart
deleted file mode 100644
index 9adf4ae..0000000
--- a/lib/screens/mains/hunt/test/hunt_test.dart
+++ /dev/null
@@ -1,28 +0,0 @@
-// Quick test to verify Hunt feature works
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-import '../hunt.dart';
-
-class HuntTestApp extends StatelessWidget {
- const HuntTestApp({super.key});
-
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Hunt Feature Test',
- theme: ThemeData(
- primarySwatch: Colors.blue,
- brightness: Brightness.light,
- ),
- darkTheme: ThemeData(
- primarySwatch: Colors.blue,
- brightness: Brightness.dark,
- ),
- home: const Hunt(), // This now properly creates its own provider
- );
- }
-}
-
-void main() {
- runApp(const HuntTestApp());
-}
diff --git a/lib/screens/mains/hunt/widgets/animated_rank_badge.dart b/lib/screens/mains/hunt/widgets/animated_rank_badge.dart
new file mode 100644
index 0000000..bed42bc
--- /dev/null
+++ b/lib/screens/mains/hunt/widgets/animated_rank_badge.dart
@@ -0,0 +1,306 @@
+import 'package:flutter/material.dart';
+import 'package:lba/res/colors.dart';
+
+class AnimatedRankBadge extends StatefulWidget {
+ final int rank;
+ final int totalPoints;
+ final String userName;
+ final bool showAnimation;
+ final VoidCallback? onTap;
+
+ const AnimatedRankBadge({
+ super.key,
+ required this.rank,
+ required this.totalPoints,
+ required this.userName,
+ this.showAnimation = true,
+ this.onTap,
+ });
+
+ @override
+ State createState() => _AnimatedRankBadgeState();
+}
+
+class _AnimatedRankBadgeState extends State
+ with TickerProviderStateMixin {
+ late AnimationController _pulseController;
+ late AnimationController _shimmerController;
+ late Animation _pulseAnimation;
+ late Animation _shimmerAnimation;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _pulseController = AnimationController(
+ duration: const Duration(milliseconds: 1500),
+ vsync: this,
+ );
+
+ _shimmerController = AnimationController(
+ duration: const Duration(milliseconds: 2000),
+ vsync: this,
+ );
+
+ _pulseAnimation = Tween(
+ begin: 1.0,
+ end: 1.1,
+ ).animate(CurvedAnimation(
+ parent: _pulseController,
+ curve: Curves.easeInOut,
+ ));
+
+ _shimmerAnimation = Tween(
+ begin: -1.0,
+ end: 1.0,
+ ).animate(CurvedAnimation(
+ parent: _shimmerController,
+ curve: Curves.easeInOut,
+ ));
+
+ if (widget.showAnimation) {
+ _pulseController.repeat(reverse: true);
+ _shimmerController.repeat();
+ }
+ }
+
+ @override
+ void dispose() {
+ _pulseController.dispose();
+ _shimmerController.dispose();
+ super.dispose();
+ }
+
+ Color _getRankColor() {
+ switch (widget.rank) {
+ case 1:
+ return const Color(0xFFFFD700);
+ case 2:
+ return const Color(0xFFC0C0C0);
+ case 3:
+ return const Color(0xFFCD7F32);
+ default:
+ if (widget.rank <= 10) {
+ return AppColors.primary;
+ } else {
+ return AppColors.textSecondary;
+ }
+ }
+ }
+
+ IconData _getRankIcon() {
+ switch (widget.rank) {
+ case 1:
+ return Icons.emoji_events_rounded;
+ case 2:
+ return Icons.emoji_events_outlined;
+ case 3:
+ return Icons.emoji_events_outlined;
+ default:
+ if (widget.rank <= 10) {
+ return Icons.military_tech_rounded;
+ } else {
+ return Icons.person_rounded;
+ }
+ }
+ }
+
+ String _getRankSuffix() {
+ if (widget.rank == 1) return 'st';
+ if (widget.rank == 2) return 'nd';
+ if (widget.rank == 3) return 'rd';
+ return 'th';
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final isTopThree = widget.rank <= 3;
+ final rankColor = _getRankColor();
+
+ return GestureDetector(
+ onTap: widget.onTap,
+ child: AnimatedBuilder(
+ animation: widget.showAnimation ? _pulseAnimation : AlwaysStoppedAnimation(1.0),
+ builder: (context, child) {
+ return Transform.scale(
+ scale: widget.showAnimation ? _pulseAnimation.value : 1.0,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: isTopThree
+ ? [
+ rankColor,
+ rankColor.withOpacity(0.8),
+ ]
+ : [
+ AppColors.cardBackground,
+ AppColors.cardBackground.withOpacity(0.8),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(
+ color: rankColor.withOpacity(0.3),
+ width: 1.5,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: rankColor.withOpacity(0.3),
+ blurRadius: isTopThree ? 12 : 6,
+ offset: const Offset(0, 4),
+ spreadRadius: isTopThree ? 2 : 0,
+ ),
+ ],
+ ),
+ child: Stack(
+ children: [
+ if (isTopThree && widget.showAnimation)
+ AnimatedBuilder(
+ animation: _shimmerAnimation,
+ builder: (context, child) {
+ return Positioned.fill(
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(18),
+ child: CustomPaint(
+ painter: ShimmerPainter(
+ progress: _shimmerAnimation.value,
+ color: Colors.white.withOpacity(0.3),
+ ),
+ size: Size.infinite,
+ ),
+ ),
+ );
+ },
+ ),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: isTopThree
+ ? Colors.white.withOpacity(0.2)
+ : rankColor.withOpacity(0.1),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Icon(
+ _getRankIcon(),
+ color: isTopThree ? Colors.white : rankColor,
+ size: 20,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(
+ '${widget.rank}${_getRankSuffix()}',
+ style: TextStyle(
+ color: isTopThree ? Colors.white : AppColors.textPrimary,
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(width: 4),
+ Text(
+ 'Place',
+ style: TextStyle(
+ color: isTopThree ? Colors.white70 : AppColors.textSecondary,
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 2),
+ Row(
+ children: [
+ Text(
+ widget.userName,
+ style: TextStyle(
+ color: isTopThree ? Colors.white70 : AppColors.textSecondary,
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
+ decoration: BoxDecoration(
+ color: isTopThree
+ ? Colors.white.withOpacity(0.2)
+ : AppColors.confirmButton.withOpacity(0.1),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.stars_rounded,
+ color: isTopThree ? Colors.white : AppColors.confirmButton,
+ size: 12,
+ ),
+ const SizedBox(width: 2),
+ Text(
+ '${widget.totalPoints}',
+ style: TextStyle(
+ color: isTopThree ? Colors.white : AppColors.confirmButton,
+ fontSize: 10,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
+
+class ShimmerPainter extends CustomPainter {
+ final double progress;
+ final Color color;
+
+ ShimmerPainter({required this.progress, required this.color});
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final paint = Paint()
+ ..shader = LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [
+ Colors.transparent,
+ color,
+ Colors.transparent,
+ ],
+ stops: const [0.0, 0.5, 1.0],
+ ).createShader(Rect.fromLTWH(
+ size.width * progress - size.width * 0.3,
+ 0,
+ size.width * 0.6,
+ size.height,
+ ));
+
+ canvas.drawRect(Offset.zero & size, paint);
+ }
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
+}
diff --git a/lib/screens/mains/hunt/widgets/celebration_overlay.dart b/lib/screens/mains/hunt/widgets/celebration_overlay.dart
new file mode 100644
index 0000000..a4f4c70
--- /dev/null
+++ b/lib/screens/mains/hunt/widgets/celebration_overlay.dart
@@ -0,0 +1,240 @@
+import 'package:flutter/material.dart';
+import 'dart:math' as math;
+
+class CelebrationOverlay extends StatefulWidget {
+ final Widget child;
+ final bool showCelebration;
+ final VoidCallback? onCelebrationComplete;
+
+ const CelebrationOverlay({
+ super.key,
+ required this.child,
+ this.showCelebration = false,
+ this.onCelebrationComplete,
+ });
+
+ @override
+ State createState() => _CelebrationOverlayState();
+}
+
+class _CelebrationOverlayState extends State
+ with TickerProviderStateMixin {
+ late AnimationController _controller;
+ late Animation _fadeAnimation;
+ late Animation _scaleAnimation;
+ late List _particles;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = AnimationController(
+ duration: const Duration(milliseconds: 2500),
+ vsync: this,
+ );
+
+ _fadeAnimation = Tween(
+ begin: 0.0,
+ end: 1.0,
+ ).animate(CurvedAnimation(
+ parent: _controller,
+ curve: const Interval(0.0, 0.3, curve: Curves.easeOut),
+ ));
+
+ _scaleAnimation = Tween(
+ begin: 0.0,
+ end: 1.0,
+ ).animate(CurvedAnimation(
+ parent: _controller,
+ curve: const Interval(0.0, 0.6, curve: Curves.elasticOut),
+ ));
+
+ _particles = _generateParticles();
+
+ _controller.addStatusListener((status) {
+ if (status == AnimationStatus.completed) {
+ widget.onCelebrationComplete?.call();
+ }
+ });
+ }
+
+ @override
+ void didUpdateWidget(CelebrationOverlay oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.showCelebration && !oldWidget.showCelebration) {
+ _startCelebration();
+ }
+ }
+
+ void _startCelebration() {
+ _controller.reset();
+ _particles = _generateParticles();
+ _controller.forward();
+ }
+
+ List _generateParticles() {
+ final random = math.Random();
+ return List.generate(30, (index) {
+ return Particle(
+ x: random.nextDouble(),
+ y: random.nextDouble(),
+ color: _getRandomColor(random),
+ size: 3.0 + random.nextDouble() * 5.0,
+ speedX: (random.nextDouble() - 0.5) * 2.0,
+ speedY: random.nextDouble() * -2.0 - 1.0,
+ );
+ });
+ }
+
+ Color _getRandomColor(math.Random random) {
+ final colors = [
+ const Color(0xFFFFD700), // Gold
+ const Color(0xFFFF6B35), // Orange
+ const Color(0xFF4ECDC4), // Teal
+ const Color(0xFF45B7D1), // Blue
+ const Color(0xFF96CEB4), // Green
+ const Color(0xFFFECEA8), // Peach
+ const Color(0xFFFF9FF3), // Pink
+ const Color(0xFFF9CA24), // Yellow
+ ];
+ return colors[random.nextInt(colors.length)];
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ children: [
+ widget.child,
+ if (widget.showCelebration)
+ AnimatedBuilder(
+ animation: _controller,
+ builder: (context, child) {
+ return Positioned.fill(
+ child: Opacity(
+ opacity: _fadeAnimation.value,
+ child: CustomPaint(
+ painter: CelebrationPainter(
+ particles: _particles,
+ progress: _controller.value,
+ scale: _scaleAnimation.value,
+ ),
+ size: Size.infinite,
+ ),
+ ),
+ );
+ },
+ ),
+ ],
+ );
+ }
+}
+
+class Particle {
+ final double x;
+ final double y;
+ final Color color;
+ final double size;
+ final double speedX;
+ final double speedY;
+
+ Particle({
+ required this.x,
+ required this.y,
+ required this.color,
+ required this.size,
+ required this.speedX,
+ required this.speedY,
+ });
+}
+
+class CelebrationPainter extends CustomPainter {
+ final List particles;
+ final double progress;
+ final double scale;
+
+ CelebrationPainter({
+ required this.particles,
+ required this.progress,
+ required this.scale,
+ });
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final paint = Paint()..style = PaintingStyle.fill;
+
+ for (final particle in particles) {
+ final currentX = particle.x * size.width + particle.speedX * progress * size.width * 0.5;
+ final currentY = particle.y * size.height + particle.speedY * progress * size.height * 0.5;
+
+ final opacity = (1.0 - progress).clamp(0.0, 1.0);
+ paint.color = particle.color.withOpacity(opacity);
+
+ final rotation = progress * math.pi * 4;
+ canvas.save();
+ canvas.translate(currentX, currentY);
+ canvas.rotate(rotation);
+
+ if (particle.size > 6) {
+ _drawStar(canvas, paint, particle.size * scale);
+ } else {
+ canvas.drawCircle(Offset.zero, particle.size * scale, paint);
+ }
+
+ canvas.restore();
+ }
+
+ if (progress < 0.3 && scale > 0) {
+ _drawBurstEffect(canvas, size, progress, scale);
+ }
+ }
+
+ void _drawStar(Canvas canvas, Paint paint, double size) {
+ final path = Path();
+ final angleStep = (math.pi * 2) / 5;
+ final innerRadius = size * 0.4;
+ final outerRadius = size;
+
+ for (int i = 0; i < 10; i++) {
+ final angle = i * angleStep / 2;
+ final radius = i % 2 == 0 ? outerRadius : innerRadius;
+ final x = math.cos(angle) * radius;
+ final y = math.sin(angle) * radius;
+
+ if (i == 0) {
+ path.moveTo(x, y);
+ } else {
+ path.lineTo(x, y);
+ }
+ }
+ path.close();
+ canvas.drawPath(path, paint);
+ }
+
+ void _drawBurstEffect(Canvas canvas, Size size, double progress, double scale) {
+ final center = Offset(size.width / 2, size.height / 2);
+ final paint = Paint()
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 3.0;
+
+ final colors = [
+ const Color(0xFFFFD700),
+ const Color(0xFFFF6B35),
+ const Color(0xFF4ECDC4),
+ ];
+
+ for (int i = 0; i < colors.length; i++) {
+ final radius = (progress * 150.0 * (i + 1)) * scale;
+ final opacity = (1.0 - progress).clamp(0.0, 1.0);
+ paint.color = colors[i].withOpacity(opacity * 0.5);
+ canvas.drawCircle(center, radius, paint);
+ }
+ }
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
+}
diff --git a/lib/screens/mains/hunt/widgets/hint_camera_widget.dart b/lib/screens/mains/hunt/widgets/hint_camera_widget.dart
index 069cb9d..fe50b4d 100644
--- a/lib/screens/mains/hunt/widgets/hint_camera_widget.dart
+++ b/lib/screens/mains/hunt/widgets/hint_camera_widget.dart
@@ -562,14 +562,12 @@ class AdvancedTargetPainter extends CustomPainter {
final primaryColor = isNear ? const Color(0xFF00E676) : const Color(0xFF2196F3);
final baseRadius = math.max(60.0, math.min(85.0, 100 - (distance / 2)));
- // Outer glow
paint
..color = primaryColor.withOpacity(0.1 * pulseValue)
..strokeWidth = 20.0
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10);
canvas.drawCircle(center, baseRadius, paint);
- // Main rotating rings
paint
..color = primaryColor.withOpacity(0.8)
..strokeWidth = 2.5
@@ -580,13 +578,11 @@ class AdvancedTargetPainter extends CustomPainter {
canvas.drawArc(Rect.fromCircle(center: center, radius: baseRadius),
rotationValue * 2 * math.pi + 3, 2.5, false, paint);
- // Inner static ring
paint
..color = primaryColor.withOpacity(0.4)
..strokeWidth = 1.5;
canvas.drawCircle(center, baseRadius - 15, paint);
- // Scanning arc
if (!isNear) {
final sweepAngle = scanValue * 2 * math.pi;
paint
@@ -601,7 +597,6 @@ class AdvancedTargetPainter extends CustomPainter {
);
}
- // Success animation
if (isNear) {
paint
..color = primaryColor.withOpacity(1.0 - pulseValue)
diff --git a/lib/screens/mains/hunt/widgets/hunt_card_widget.dart b/lib/screens/mains/hunt/widgets/hunt_card_widget.dart
index 37d879e..6ced90f 100644
--- a/lib/screens/mains/hunt/widgets/hunt_card_widget.dart
+++ b/lib/screens/mains/hunt/widgets/hunt_card_widget.dart
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:lba/gen/assets.gen.dart';
import 'package:lba/res/colors.dart';
import '../models/hunt_card.dart';
import 'hunt_timer_widget.dart';
@@ -98,7 +100,6 @@ class _HuntCardWidgetState extends State
}
void _openARHint() {
- // Navigate to AR camera for location-based hints
Navigator.push(
context,
MaterialPageRoute(
@@ -367,7 +368,6 @@ class _HuntCardWidgetState extends State
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- // Gaming-style icon container with floating effect
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
@@ -414,7 +414,6 @@ class _HuntCardWidgetState extends State
),
),
const SizedBox(height: 15),
- // Gaming-style mystery text with holographic effect
Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
decoration: BoxDecoration(
@@ -491,7 +490,6 @@ class _HuntCardWidgetState extends State
),
),
),
- // Gaming-style difficulty indicator with glow
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (index) {
@@ -557,7 +555,6 @@ class _HuntCardWidgetState extends State
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // Header with title and timer
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -607,7 +604,6 @@ class _HuntCardWidgetState extends State
),
],
),
- // Enhanced Timer
HuntTimerWidget(
timeRemaining: const Duration(hours: 12),
isActive: true,
@@ -619,7 +615,6 @@ class _HuntCardWidgetState extends State
const SizedBox(height: 10),
- // Riddle section
Expanded(
flex: 4,
child: Container(
@@ -694,7 +689,6 @@ class _HuntCardWidgetState extends State
const SizedBox(height: 8),
- // AR Hint section
Expanded(
flex: 1,
child: GestureDetector(
@@ -732,14 +726,13 @@ class _HuntCardWidgetState extends State
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Icon(
- Icons.help,
+ SvgPicture.asset(
+ Assets.icons.infoCircle.path,
color: AppColors.isDarkMode
? Colors.yellow.shade600
: const Color(0xFFE65100),
- size: 14,
),
- const SizedBox(width: 6),
+ const SizedBox(width: 3),
Text(
'AR Smart Hint',
style: TextStyle(
@@ -758,7 +751,6 @@ class _HuntCardWidgetState extends State
const SizedBox(height: 6),
- // Action button
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 6),
@@ -790,10 +782,10 @@ class _HuntCardWidgetState extends State
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Icon(
- Icons.location_on_rounded,
+ SvgPicture.asset(
+ Assets.icons.locationTick.path,
color: Colors.white,
- size: 12,
+ width: 14,
),
const SizedBox(width: 4),
Text(
@@ -819,7 +811,6 @@ class _HuntCardWidgetState extends State
final pi = 3.14159;
if (isShowingFront) {
- // Front side animated gradient
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
@@ -863,7 +854,6 @@ class _HuntCardWidgetState extends State
],
);
} else {
- // Back side animated gradient
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
diff --git a/lib/screens/mains/hunt/widgets/hunt_timer_widget.dart b/lib/screens/mains/hunt/widgets/hunt_timer_widget.dart
index f774db5..0f10a57 100644
--- a/lib/screens/mains/hunt/widgets/hunt_timer_widget.dart
+++ b/lib/screens/mains/hunt/widgets/hunt_timer_widget.dart
@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:lba/gen/assets.gen.dart';
import 'package:lba/res/colors.dart';
class HuntTimerWidget extends StatefulWidget {
@@ -75,7 +77,6 @@ class _HuntTimerWidgetState extends State
if (_currentTime.inSeconds > 0) {
_currentTime = Duration(seconds: _currentTime.inSeconds - 1);
- // Pulse animation when time is running low
if (_currentTime.inMinutes < 5) {
_pulseController.forward().then((_) {
_pulseController.reverse();
@@ -158,14 +159,14 @@ class _HuntTimerWidgetState extends State
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
- Icon(
+ SvgPicture.asset(
isExpired
- ? Icons.timer_off_outlined
+ ? Assets.icons.timerHunt.path
: _currentTime.inMinutes < 2
- ? Icons.timer_outlined
- : Icons.timer_rounded,
+ ? Assets.icons.timerHunt.path
+ : Assets.icons.timerHunt.path,
color: timerColor,
- size: widget.fontSize != null ? widget.fontSize! + 2 : 14,
+ width: widget.fontSize != null ? widget.fontSize! + 4 : 16,
),
const SizedBox(width: 4),
Text(
diff --git a/lib/screens/mains/hunt/widgets/leaderboard_widget.dart b/lib/screens/mains/hunt/widgets/leaderboard_widget.dart
index 75f4f02..7420160 100644
--- a/lib/screens/mains/hunt/widgets/leaderboard_widget.dart
+++ b/lib/screens/mains/hunt/widgets/leaderboard_widget.dart
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:lba/gen/assets.gen.dart';
import 'package:lba/res/colors.dart';
import '../models/hunt_card.dart';
@@ -6,12 +8,14 @@ class LeaderboardWidget extends StatefulWidget {
final List entries;
final int userPoints;
final VoidCallback onClose;
+ final VoidCallback? onRefresh;
const LeaderboardWidget({
super.key,
required this.entries,
required this.userPoints,
required this.onClose,
+ this.onRefresh,
});
@override
@@ -22,8 +26,15 @@ class _LeaderboardWidgetState extends State
with TickerProviderStateMixin {
late AnimationController _slideController;
late AnimationController _itemController;
+ late AnimationController _particleController;
+ late AnimationController _trophyController;
late Animation _slideAnimation;
late List> _itemAnimations;
+ late Animation _particleAnimation;
+ late Animation _trophyGlowAnimation;
+
+ String _searchQuery = '';
+ bool _showSearch = false;
@override
void initState() {
@@ -36,38 +47,56 @@ class _LeaderboardWidgetState extends State
duration: const Duration(milliseconds: 1200),
vsync: this,
);
+ _particleController = AnimationController(
+ duration: const Duration(milliseconds: 3000),
+ vsync: this,
+ );
+ _trophyController = AnimationController(
+ duration: const Duration(milliseconds: 2000),
+ vsync: this,
+ );
_slideAnimation = Tween(
begin: const Offset(0, 1),
end: Offset.zero,
- ).animate(CurvedAnimation(
- parent: _slideController,
- curve: Curves.easeOutCubic,
- ));
+ ).animate(
+ CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
+ );
_itemAnimations = List.generate(
widget.entries.length,
- (index) => Tween(
- begin: 0.0,
- end: 1.0,
- ).animate(CurvedAnimation(
- parent: _itemController,
- curve: Interval(
- (index * 0.1).clamp(0.0, 0.4),
- ((index * 0.1) + 0.6).clamp(0.1, 1.0),
- curve: Curves.easeOutBack,
+ (index) => Tween(begin: 0.0, end: 1.0).animate(
+ CurvedAnimation(
+ parent: _itemController,
+ curve: Interval(
+ (index * 0.08).clamp(0.0, 0.3),
+ ((index * 0.08) + 0.7).clamp(0.1, 1.0),
+ curve: Curves.elasticOut,
+ ),
),
- )),
+ ),
+ );
+
+ _particleAnimation = Tween(begin: 0.0, end: 1.0).animate(
+ CurvedAnimation(parent: _particleController, curve: Curves.linear),
+ );
+
+ _trophyGlowAnimation = Tween(begin: 0.0, end: 1.0).animate(
+ CurvedAnimation(parent: _trophyController, curve: Curves.easeInOut),
);
_slideController.forward();
_itemController.forward();
+ _particleController.repeat();
+ _trophyController.repeat(reverse: true);
}
@override
void dispose() {
_slideController.dispose();
_itemController.dispose();
+ _particleController.dispose();
+ _trophyController.dispose();
super.dispose();
}
@@ -78,81 +107,73 @@ class _LeaderboardWidgetState extends State
@override
Widget build(BuildContext context) {
+ final screenHeight = MediaQuery.of(context).size.height;
+ final statusBarHeight = MediaQuery.of(context).padding.top;
+ final bottomPadding = MediaQuery.of(context).padding.bottom;
+ final availableHeight =
+ screenHeight - statusBarHeight - bottomPadding - 100;
+
return SlideTransition(
position: _slideAnimation,
child: Container(
- height: MediaQuery.of(context).size.height * 0.7,
+ height: availableHeight,
decoration: BoxDecoration(
- color: AppColors.surface,
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [
+ AppColors.surface,
+ AppColors.cardBackground.withOpacity(0.8),
+ AppColors.surface,
+ ],
+ stops: const [0.0, 0.5, 1.0],
+ ),
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [
BoxShadow(
- color: AppColors.shadowColor,
+ color: AppColors.shadowColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, -10),
+ spreadRadius: 2,
),
],
),
- child: Column(
+ child: Stack(
children: [
- // Handle bar
- Container(
- margin: const EdgeInsets.symmetric(vertical: 12),
- height: 4,
- width: 40,
- decoration: BoxDecoration(
- color: AppColors.divider,
- borderRadius: BorderRadius.circular(2),
- ),
- ),
- // Header
- Padding(
- padding: const EdgeInsets.fromLTRB(24, 8, 24, 16),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- 'Leaderboard',
- style: TextStyle(
- color: AppColors.textPrimary,
- fontSize: 24,
- fontWeight: FontWeight.bold,
+ AnimatedBuilder(
+ animation: _particleAnimation,
+ builder:
+ (context, child) => CustomPaint(
+ painter: ParticlesPainter(
+ animation: _particleAnimation,
+ color: AppColors.primary.withOpacity(0.1),
),
+ size: Size.infinite,
),
- IconButton(
- onPressed: _close,
- icon: Icon(
- Icons.close_rounded,
- color: AppColors.textSecondary,
- ),
- ),
- ],
- ),
),
- // Leaderboard list
- Expanded(
- child: ListView.builder(
- padding: const EdgeInsets.symmetric(horizontal: 24),
- itemCount: widget.entries.length,
- itemBuilder: (context, index) {
- final entry = widget.entries[index];
- return AnimatedBuilder(
- animation: _itemAnimations[index],
- builder: (context, child) {
- return Transform.translate(
- offset: Offset(
- 0,
- 50 * (1 - _itemAnimations[index].value),
- ),
- child: Opacity(
- opacity: _itemAnimations[index].value.clamp(0.0, 1.0),
- child: _buildLeaderboardItem(entry, index),
- ),
- );
- },
- );
- },
- ),
+ Column(
+ children: [
+ // Handle bar
+ Container(
+ margin: const EdgeInsets.symmetric(vertical: 4),
+ height: 3,
+ width: 32,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ AppColors.primary.withOpacity(0.3),
+ AppColors.primary,
+ AppColors.primary.withOpacity(0.3),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(2),
+ ),
+ ),
+ _buildCompactHeader(),
+ if (widget.entries.length > 10) _buildQuickSearch(),
+ if (widget.entries.length >= 3) _buildPodium(),
+ Expanded(child: _buildParticipantsList()),
+ ],
),
],
),
@@ -160,116 +181,236 @@ class _LeaderboardWidgetState extends State
);
}
- Widget _buildLeaderboardItem(LeaderboardEntry entry, int index) {
- final isCurrentUser = entry.isCurrentUser;
- final isTopThree = entry.rank <= 3;
-
- return Container(
- margin: const EdgeInsets.only(bottom: 12),
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: isCurrentUser
- ? AppColors.primary.withOpacity(0.1)
- : AppColors.cardBackground,
- borderRadius: BorderRadius.circular(16),
- border: isCurrentUser
- ? Border.all(color: AppColors.primary, width: 2)
- : null,
- boxShadow: isTopThree
- ? [
- BoxShadow(
- color: AppColors.shadowColor,
- blurRadius: 8,
- offset: const Offset(0, 4),
- ),
- ]
- : null,
- ),
+ Widget _buildCompactHeader() {
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(20, 2, 20, 4),
child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- // Rank
- Container(
- width: 40,
- height: 40,
- decoration: BoxDecoration(
- color: _getRankColor(entry.rank),
- shape: BoxShape.circle,
- ),
- child: Center(
- child: Text(
- '#${entry.rank}',
- style: TextStyle(
- color: entry.rank <= 3 ? Colors.white : AppColors.textPrimary,
- fontWeight: FontWeight.bold,
- fontSize: 14,
- ),
- ),
- ),
- ),
- const SizedBox(width: 16),
- // Avatar
- Container(
- width: 50,
- height: 50,
- decoration: BoxDecoration(
- color: AppColors.primary.withOpacity(0.1),
- shape: BoxShape.circle,
- ),
- child: Center(
- child: Text(
- entry.avatar,
- style: const TextStyle(fontSize: 24),
- ),
- ),
- ),
- const SizedBox(width: 16),
- // Name and points
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- entry.name,
- style: TextStyle(
- color: AppColors.textPrimary,
- fontSize: 16,
- fontWeight: isCurrentUser ? FontWeight.bold : FontWeight.w600,
+ Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ AppColors.primary,
+ AppColors.primary.withOpacity(0.7),
+ ],
),
- ),
- const SizedBox(height: 4),
- Row(
- children: [
- Icon(
- Icons.stars_rounded,
- color: AppColors.confirmButton,
- size: 16,
- ),
- const SizedBox(width: 4),
- Text(
- '${entry.totalPoints} points',
- style: TextStyle(
- color: AppColors.textSecondary,
- fontSize: 14,
- fontWeight: FontWeight.w500,
- ),
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [
+ BoxShadow(
+ color: AppColors.primary.withOpacity(0.2),
+ blurRadius: 8,
+ offset: const Offset(0, 2),
),
],
),
- ],
- ),
+ child: SvgPicture.asset(
+ Assets.icons.cup.path,
+ color: Colors.white,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(
+ 'Hunt Leaderboard',
+ style: TextStyle(
+ color: AppColors.textPrimary,
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ letterSpacing: -0.3,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 6,
+ vertical: 2,
+ ),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ AppColors.confirmButton.withOpacity(0.2),
+ AppColors.confirmButton.withOpacity(0.1),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(
+ color: AppColors.confirmButton.withOpacity(0.3),
+ width: 0.5,
+ ),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 4,
+ height: 4,
+ decoration: BoxDecoration(
+ color: AppColors.confirmButton,
+ shape: BoxShape.circle,
+ ),
+ ),
+ const SizedBox(width: 4),
+ Text(
+ '${widget.entries.length}',
+ style: TextStyle(
+ color: AppColors.confirmButton,
+ fontSize: 10,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ Text(
+ 'Live ranking updates',
+ style: TextStyle(
+ color: AppColors.textSecondary,
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ],
),
- // Trophy for top 3
- if (isTopThree)
- Container(
- padding: const EdgeInsets.all(8),
+ IconButton(
+ onPressed: _close,
+ padding: const EdgeInsets.all(4),
+ icon: Container(
+ padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
- color: _getRankColor(entry.rank).withOpacity(0.1),
- borderRadius: BorderRadius.circular(8),
+ color: AppColors.cardBackground,
+ borderRadius: BorderRadius.circular(10),
+ boxShadow: [
+ BoxShadow(
+ color: AppColors.shadowColor.withOpacity(0.1),
+ blurRadius: 4,
+ offset: const Offset(0, 1),
+ ),
+ ],
),
child: Icon(
- Icons.emoji_events_rounded,
- color: _getRankColor(entry.rank),
- size: 24,
+ Icons.close_rounded,
+ color: AppColors.textSecondary,
+ size: 20,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ String _formatNumber(int number) {
+ if (number >= 1000) {
+ return '${(number / 1000).toStringAsFixed(1)}k';
+ }
+ return number.toString();
+ }
+
+ String _getShortName(String fullName) {
+ if (fullName.length <= 10) return fullName;
+
+ final parts = fullName.split(' ');
+ if (parts.length > 1) {
+ final firstName = parts[0];
+ final lastInitial = parts[1].isNotEmpty ? parts[1][0] : '';
+ if (firstName.length <= 8) {
+ return '$firstName $lastInitial.';
+ }
+ return '${firstName.substring(0, 8)}...';
+ }
+
+ return '${fullName.substring(0, 8)}...';
+ }
+
+ Widget _buildQuickSearch() {
+ return AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ height: _showSearch ? 60 : 40,
+ margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: AppColors.cardBackground,
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(color: AppColors.primary.withOpacity(0.1), width: 1),
+ ),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ Icon(Icons.search_rounded, color: AppColors.primary, size: 20),
+ const SizedBox(width: 8),
+ Expanded(
+ child:
+ _showSearch
+ ? TextField(
+ onChanged:
+ (value) => setState(() => _searchQuery = value),
+ style: TextStyle(
+ color: AppColors.textPrimary,
+ fontSize: 14,
+ ),
+ decoration: InputDecoration(
+ hintText: 'Search hunters...',
+ hintStyle: TextStyle(
+ color: AppColors.textSecondary,
+ fontSize: 14,
+ ),
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.zero,
+ ),
+ )
+ : GestureDetector(
+ onTap: () => setState(() => _showSearch = true),
+ child: Text(
+ 'Search in ${widget.entries.length} hunters',
+ style: TextStyle(
+ color: AppColors.textSecondary,
+ fontSize: 14,
+ ),
+ ),
+ ),
+ ),
+ if (_showSearch)
+ IconButton(
+ onPressed:
+ () => setState(() {
+ _showSearch = false;
+ _searchQuery = '';
+ }),
+ icon: Icon(
+ Icons.close_rounded,
+ color: AppColors.textSecondary,
+ size: 18,
+ ),
+ padding: const EdgeInsets.all(4),
+ constraints: const BoxConstraints(
+ minWidth: 24,
+ minHeight: 24,
+ ),
+ ),
+ ],
+ ),
+ if (_showSearch && _searchQuery.isNotEmpty)
+ Expanded(
+ child: Text(
+ '${_getFilteredEntries().length} results found',
+ style: TextStyle(
+ color: AppColors.primary,
+ fontSize: 11,
+ fontWeight: FontWeight.w500,
+ ),
),
),
],
@@ -277,16 +418,595 @@ class _LeaderboardWidgetState extends State
);
}
- Color _getRankColor(int rank) {
- switch (rank) {
+ List _getFilteredEntries() {
+ if (_searchQuery.isEmpty) return widget.entries;
+ return widget.entries
+ .where(
+ (entry) =>
+ entry.name.toLowerCase().contains(_searchQuery.toLowerCase()),
+ )
+ .toList();
+ }
+
+ Widget _buildListView(List remainingParticipants) {
+ return ListView.builder(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ itemCount: remainingParticipants.length,
+ itemExtent: 70,
+ itemBuilder: (context, index) {
+ final entry = remainingParticipants[index];
+ final actualIndex = index + 3;
+
+ return AnimatedBuilder(
+ animation:
+ _itemAnimations.length > actualIndex
+ ? _itemAnimations[actualIndex]
+ : AlwaysStoppedAnimation(1.0),
+ builder: (context, child) {
+ final animation =
+ _itemAnimations.length > actualIndex
+ ? _itemAnimations[actualIndex]
+ : AlwaysStoppedAnimation(1.0);
+
+ return Transform.translate(
+ offset: Offset(0, 20 * (1 - animation.value)),
+ child: Opacity(
+ opacity: animation.value.clamp(0.0, 1.0),
+ child: _buildCompactLeaderboardItem(entry, actualIndex + 1),
+ ),
+ );
+ },
+ );
+ },
+ );
+ }
+
+ Widget _buildPodium() {
+ final topThree = widget.entries.take(3).toList();
+
+ return Container(
+ margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
+ height: 150,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ if (topThree.length > 1) _buildPodiumPlace(topThree[1], 2, 105),
+ _buildPodiumPlace(topThree[0], 1, 130),
+ if (topThree.length > 2) _buildPodiumPlace(topThree[2], 3, 85),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildPodiumPlace(LeaderboardEntry entry, int place, double height) {
+ final colors = _getPodiumColors(place);
+
+ return AnimatedBuilder(
+ animation: _trophyGlowAnimation,
+ builder: (context, child) {
+ return GestureDetector(
+ onTap: () {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Container(
+ padding: const EdgeInsets.all(4),
+ child: Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(6),
+ decoration: BoxDecoration(
+ color: colors['primary'],
+ shape: BoxShape.circle,
+ ),
+ child: Text(
+ '#$place',
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 12,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Row(
+ children: [
+ Expanded(
+ child: Text(
+ entry.name,
+ style: TextStyle(
+ color: AppColors.textPrimary,
+ fontWeight: FontWeight.bold,
+ fontSize: 15,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ Text(
+ '${entry.totalPoints}',
+ style: TextStyle(
+ color: AppColors.primary,
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Container(
+ padding: const EdgeInsets.all(4),
+ decoration: BoxDecoration(
+ color: AppColors.primary.withOpacity(0.1),
+ shape: BoxShape.circle,
+ ),
+ child: Text(
+ entry.avatar,
+ style: const TextStyle(fontSize: 18),
+ ),
+ ),
+ ],
+ ),
+ ),
+ backgroundColor: AppColors.surface,
+ behavior: SnackBarBehavior.floating,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ elevation: 8,
+ margin: const EdgeInsets.all(16),
+ duration: const Duration(seconds: 2),
+ ),
+ );
+ },
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.end,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (place == 1) ...[
+ Container(
+ width: 24,
+ height: 24,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: RadialGradient(
+ colors: [
+ colors['primary']!.withOpacity(0.8),
+ colors['primary']!,
+ ],
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: colors['primary']!.withOpacity(
+ 0.3 + (_trophyGlowAnimation.value * 0.2),
+ ),
+ blurRadius: 10 + (_trophyGlowAnimation.value * 5),
+ spreadRadius: 1,
+ ),
+ ],
+ ),
+ child: Icon(
+ Icons.emoji_events_rounded,
+ color: Colors.white,
+ size: 12,
+ ),
+ ),
+ const SizedBox(height: 1),
+ ] else
+ const SizedBox(height: 1),
+ Container(
+ width: 70,
+ height: height,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [colors['light']!, colors['primary']!],
+ ),
+ borderRadius: const BorderRadius.vertical(
+ top: Radius.circular(10),
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: colors['primary']!.withOpacity(0.25),
+ blurRadius: 6,
+ offset: const Offset(0, 3),
+ ),
+ ],
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 1,
+ vertical: 1,
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SizedBox(
+ width: 24,
+ height: 24,
+ child: Center(
+ child: Text(
+ entry.avatar,
+ style: const TextStyle(fontSize: 10),
+ ),
+ ),
+ ),
+ SizedBox(
+ height: place == 1 ? 14 : 12,
+ child: Center(
+ child: Text(
+ _getShortName(entry.name),
+ style: TextStyle(
+ color: place == 1 ? Colors.black : Colors.white,
+ fontSize: place == 1 ? 12 : 10,
+ fontWeight:
+ place == 1
+ ? FontWeight.w900
+ : FontWeight.bold,
+ height: 1.0,
+ ),
+ textAlign: TextAlign.center,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ),
+ Text(
+ _formatNumber(entry.totalPoints),
+ style: TextStyle(
+ color: place == 1 ? Colors.black : Colors.white,
+ fontSize: place == 1 ? 13 : 11,
+ fontWeight:
+ place == 1 ? FontWeight.w900 : FontWeight.bold,
+ ),
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 5,
+ vertical: 1,
+ ),
+ decoration: BoxDecoration(
+ color: Colors.white.withOpacity(0.3),
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Text(
+ '#$place',
+ style: TextStyle(
+ color: place == 1 ? Colors.black : Colors.white,
+ fontSize: place == 1 ? 10 : 9,
+ fontWeight:
+ place == 1 ? FontWeight.w900 : FontWeight.bold,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+
+ Map _getPodiumColors(int place) {
+ switch (place) {
case 1:
- return const Color(0xFFFFD700); // Gold
+ return {
+ 'primary': const Color(0xFFFFD700),
+ 'light': const Color(0xFFFFFACD),
+ };
case 2:
- return const Color(0xFFC0C0C0); // Silver
+ return {
+ 'primary': const Color(0xFFC0C0C0),
+ 'light': const Color(0xFFE8E8E8),
+ };
case 3:
- return const Color(0xFFCD7F32); // Bronze
+ return {
+ 'primary': const Color(0xFFCD7F32),
+ 'light': const Color(0xFFDEB887),
+ };
default:
- return AppColors.primary;
+ return {
+ 'primary': AppColors.primary,
+ 'light': AppColors.primary.withOpacity(0.3),
+ };
}
}
+
+ Widget _buildParticipantsList() {
+ final filteredEntries = _getFilteredEntries();
+ final remainingParticipants =
+ filteredEntries.length > 3
+ ? filteredEntries.skip(3).toList()
+ : [];
+
+ if (_searchQuery.isNotEmpty) {
+ final allFiltered = filteredEntries.toList();
+ if (allFiltered.isEmpty) {
+ return Expanded(
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.search_off_rounded,
+ color: AppColors.textSecondary,
+ size: 48,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'No hunters found',
+ style: TextStyle(
+ color: AppColors.textSecondary,
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Try a different search term',
+ style: TextStyle(
+ color: AppColors.textSecondary,
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+ remainingParticipants.clear();
+ remainingParticipants.addAll(allFiltered);
+ }
+
+ if (remainingParticipants.isEmpty) {
+ return const SizedBox.shrink();
+ }
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 8, 20, 6),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ _searchQuery.isNotEmpty
+ ? 'Search Results'
+ : 'Other Participants',
+ style: TextStyle(
+ color: AppColors.textPrimary,
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 2,
+ ),
+ decoration: BoxDecoration(
+ color: AppColors.primary.withOpacity(0.1),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Text(
+ '${widget.entries.length} hunters',
+ style: TextStyle(
+ color: AppColors.primary,
+ fontSize: 11,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ Expanded(
+ child:
+ widget.onRefresh != null
+ ? RefreshIndicator(
+ onRefresh: () async {
+ widget.onRefresh?.call();
+ await Future.delayed(const Duration(milliseconds: 500));
+ },
+ color: AppColors.primary,
+ backgroundColor: AppColors.surface,
+ child: _buildListView(remainingParticipants),
+ )
+ : _buildListView(remainingParticipants),
+ ),
+ SizedBox(height: MediaQuery.of(context).padding.bottom + 20),
+ ],
+ );
+ }
+
+ Widget _buildCompactLeaderboardItem(LeaderboardEntry entry, int index) {
+ final isCurrentUser = entry.isCurrentUser;
+
+ return Container(
+ margin: const EdgeInsets.only(bottom: 6),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ decoration: BoxDecoration(
+ gradient:
+ isCurrentUser
+ ? LinearGradient(
+ colors: [
+ AppColors.primary.withOpacity(0.08),
+ AppColors.primary.withOpacity(0.04),
+ ],
+ )
+ : null,
+ color: isCurrentUser ? null : AppColors.cardBackground,
+ borderRadius: BorderRadius.circular(14),
+ border:
+ isCurrentUser
+ ? Border.all(
+ color: AppColors.primary.withOpacity(0.2),
+ width: 1,
+ )
+ : null,
+ boxShadow: [
+ BoxShadow(
+ color:
+ isCurrentUser
+ ? AppColors.primary.withOpacity(0.08)
+ : AppColors.shadowColor.withOpacity(0.03),
+ blurRadius: isCurrentUser ? 8 : 3,
+ offset: const Offset(0, 1),
+ ),
+ ],
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ color: AppColors.cardBackground,
+ shape: BoxShape.circle,
+ border: Border.all(
+ color: AppColors.divider.withOpacity(0.3),
+ width: 0.8,
+ ),
+ ),
+ child: Center(
+ child: Text(
+ '#$index',
+ style: TextStyle(
+ color: AppColors.textPrimary,
+ fontWeight: FontWeight.bold,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Container(
+ width: 36,
+ height: 36,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ AppColors.primary.withOpacity(0.15),
+ AppColors.primary.withOpacity(0.08),
+ ],
+ ),
+ shape: BoxShape.circle,
+ border: Border.all(
+ color: AppColors.primary.withOpacity(0.2),
+ width: 0.8,
+ ),
+ ),
+ child: Center(
+ child: Text(entry.avatar, style: const TextStyle(fontSize: 18)),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Row(
+ children: [
+ Expanded(
+ child: Text(
+ entry.name.length > 12
+ ? '${entry.name.substring(0, 12)}...'
+ : entry.name,
+ style: TextStyle(
+ color: AppColors.textPrimary,
+ fontSize: 14,
+ fontWeight:
+ isCurrentUser ? FontWeight.bold : FontWeight.w600,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ if (isCurrentUser) ...[
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 6,
+ vertical: 1,
+ ),
+ decoration: BoxDecoration(
+ color: AppColors.primary,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Text(
+ 'You',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 9,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ ],
+ Text(
+ '${_formatNumber(entry.totalPoints)}',
+ style: TextStyle(
+ color: AppColors.primary,
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ ),
+ ),
+ if (entry.totalPoints > 500)
+ Container(
+ padding: const EdgeInsets.all(4),
+ decoration: BoxDecoration(
+ color: AppColors.confirmButton.withOpacity(0.1),
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Icon(
+ Icons.local_fire_department_rounded,
+ color: AppColors.confirmButton,
+ size: 14,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class ParticlesPainter extends CustomPainter {
+ final Animation animation;
+ final Color color;
+
+ ParticlesPainter({required this.animation, required this.color});
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final paint =
+ Paint()
+ ..color = color
+ ..style = PaintingStyle.fill;
+
+ final particleCount = 20;
+
+ for (int i = 0; i < particleCount; i++) {
+ final progress = (animation.value + (i / particleCount)) % 1.0;
+ final x = (i * 37.0) % size.width;
+ final y = size.height * progress;
+ final radius = (2.0 + (i % 3)) * (1.0 - progress);
+
+ paint.color = color.withOpacity((1.0 - progress) * 0.3);
+ canvas.drawCircle(Offset(x, y), radius, paint);
+ }
+ }
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}