1013 lines
34 KiB
Dart
1013 lines
34 KiB
Dart
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';
|
|
|
|
class LeaderboardWidget extends StatefulWidget {
|
|
final List<LeaderboardEntry> 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
|
|
State<LeaderboardWidget> createState() => _LeaderboardWidgetState();
|
|
}
|
|
|
|
class _LeaderboardWidgetState extends State<LeaderboardWidget>
|
|
with TickerProviderStateMixin {
|
|
late AnimationController _slideController;
|
|
late AnimationController _itemController;
|
|
late AnimationController _particleController;
|
|
late AnimationController _trophyController;
|
|
late Animation<Offset> _slideAnimation;
|
|
late List<Animation<double>> _itemAnimations;
|
|
late Animation<double> _particleAnimation;
|
|
late Animation<double> _trophyGlowAnimation;
|
|
|
|
String _searchQuery = '';
|
|
bool _showSearch = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_slideController = AnimationController(
|
|
duration: const Duration(milliseconds: 600),
|
|
vsync: this,
|
|
);
|
|
_itemController = AnimationController(
|
|
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<Offset>(
|
|
begin: const Offset(0, 1),
|
|
end: Offset.zero,
|
|
).animate(
|
|
CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
|
|
);
|
|
|
|
_itemAnimations = List.generate(
|
|
widget.entries.length,
|
|
(index) => Tween<double>(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<double>(begin: 0.0, end: 1.0).animate(
|
|
CurvedAnimation(parent: _particleController, curve: Curves.linear),
|
|
);
|
|
|
|
_trophyGlowAnimation = Tween<double>(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();
|
|
}
|
|
|
|
Future<void> _close() async {
|
|
await _slideController.reverse();
|
|
widget.onClose();
|
|
}
|
|
|
|
@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: availableHeight,
|
|
decoration: BoxDecoration(
|
|
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.withOpacity(0.3),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, -10),
|
|
spreadRadius: 2,
|
|
),
|
|
],
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
AnimatedBuilder(
|
|
animation: _particleAnimation,
|
|
builder:
|
|
(context, child) => CustomPaint(
|
|
painter: ParticlesPainter(
|
|
animation: _particleAnimation,
|
|
color: AppColors.primary.withOpacity(0.1),
|
|
),
|
|
size: Size.infinite,
|
|
),
|
|
),
|
|
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()),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCompactHeader() {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 2, 20, 4),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
AppColors.primary,
|
|
AppColors.primary.withOpacity(0.7),
|
|
],
|
|
),
|
|
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: 6,
|
|
height: 6,
|
|
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
IconButton(
|
|
onPressed: _close,
|
|
padding: const EdgeInsets.all(4),
|
|
icon: Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
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.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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
List<LeaderboardEntry> _getFilteredEntries() {
|
|
if (_searchQuery.isEmpty) return widget.entries;
|
|
return widget.entries
|
|
.where(
|
|
(entry) =>
|
|
entry.name.toLowerCase().contains(_searchQuery.toLowerCase()),
|
|
)
|
|
.toList();
|
|
}
|
|
|
|
Widget _buildListView(List<LeaderboardEntry> 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: 3),
|
|
] else
|
|
const SizedBox(height: 3),
|
|
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<String, Color> _getPodiumColors(int place) {
|
|
switch (place) {
|
|
case 1:
|
|
return {
|
|
'primary': const Color(0xFFFFD700),
|
|
'light': const Color(0xFFFFFACD),
|
|
};
|
|
case 2:
|
|
return {
|
|
'primary': const Color(0xFFC0C0C0),
|
|
'light': const Color(0xFFE8E8E8),
|
|
};
|
|
case 3:
|
|
return {
|
|
'primary': const Color(0xFFCD7F32),
|
|
'light': const Color(0xFFDEB887),
|
|
};
|
|
default:
|
|
return {
|
|
'primary': AppColors.primary,
|
|
'light': AppColors.primary.withOpacity(0.3),
|
|
};
|
|
}
|
|
}
|
|
|
|
Widget _buildParticipantsList() {
|
|
final filteredEntries = _getFilteredEntries();
|
|
final remainingParticipants =
|
|
filteredEntries.length > 3
|
|
? filteredEntries.skip(3).toList()
|
|
: <LeaderboardEntry>[];
|
|
|
|
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<double> 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;
|
|
}
|