1096 lines
44 KiB
Dart
1096 lines
44 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:playmaker/classe/theme.dart';
|
|
import 'package:playmaker/grafico%20de%20pizza/grafico.dart';
|
|
import 'package:playmaker/pages/gamePage.dart';
|
|
import 'package:playmaker/pages/teamPage.dart';
|
|
import 'package:playmaker/controllers/team_controller.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:playmaker/pages/status_page.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../utils/size_extension.dart';
|
|
import 'settings_screen.dart';
|
|
|
|
class HomeScreen extends StatefulWidget {
|
|
const HomeScreen({super.key});
|
|
|
|
@override
|
|
State<HomeScreen> createState() => _HomeScreenState();
|
|
}
|
|
|
|
class _HomeScreenState extends State<HomeScreen> {
|
|
int _selectedIndex = 0;
|
|
final TeamController _teamController = TeamController();
|
|
|
|
String? _selectedTeamId;
|
|
String _selectedTeamName = "Selecionar Equipa";
|
|
String? _selectedTeamLogo;
|
|
|
|
int _teamWins = 0;
|
|
int _teamLosses = 0;
|
|
int _teamDraws = 0;
|
|
|
|
final _supabase = Supabase.instance.client;
|
|
|
|
String? _avatarUrl;
|
|
bool _isMemoryLoaded = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadUserAvatar();
|
|
_loadSelectedTeam();
|
|
}
|
|
|
|
String _prefsKey(String key) {
|
|
final userId = _supabase.auth.currentUser?.id ?? 'guest';
|
|
return '${key}_$userId';
|
|
}
|
|
|
|
Future<void> _loadSelectedTeam() async {
|
|
// 1. Carrega primeiro do SharedPreferences para resposta imediata
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final savedId = prefs.getString(_prefsKey('last_team_id'));
|
|
|
|
if (savedId != null && mounted) {
|
|
setState(() {
|
|
_selectedTeamId = savedId;
|
|
_selectedTeamName =
|
|
prefs.getString(_prefsKey('last_team_name')) ?? "Selecionar Equipa";
|
|
_selectedTeamLogo = prefs.getString(_prefsKey('last_team_logo'));
|
|
_teamWins = prefs.getInt(_prefsKey('last_team_wins')) ?? 0;
|
|
_teamLosses = prefs.getInt(_prefsKey('last_team_losses')) ?? 0;
|
|
_teamDraws = prefs.getInt(_prefsKey('last_team_draws')) ?? 0;
|
|
});
|
|
}
|
|
|
|
// 2. Depois sincroniza com o Supabase para ter os dados mais recentes
|
|
final userId = _supabase.auth.currentUser?.id;
|
|
if (userId == null) return;
|
|
|
|
try {
|
|
final profile = await _supabase
|
|
.from('profiles')
|
|
.select('selected_team_id')
|
|
.eq('id', userId)
|
|
.maybeSingle();
|
|
|
|
if (profile != null && profile['selected_team_id'] != null) {
|
|
final dbTeamId = profile['selected_team_id'].toString();
|
|
final teamData = await _supabase
|
|
.from('teams')
|
|
.select()
|
|
.eq('id', dbTeamId)
|
|
.maybeSingle();
|
|
|
|
if (teamData != null && mounted) {
|
|
setState(() {
|
|
_selectedTeamId = teamData['id'].toString();
|
|
_selectedTeamName = teamData['name'] ?? 'Desconhecido';
|
|
_selectedTeamLogo = teamData['image_url'];
|
|
_teamWins =
|
|
int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0;
|
|
_teamLosses =
|
|
int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0;
|
|
_teamDraws =
|
|
int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0;
|
|
});
|
|
await _saveToSharedPreferences();
|
|
} else if (mounted) {
|
|
// Se o utilizador não tem equipa selecionada, limpa valores locais para não mostrar dados de outra conta.
|
|
setState(() {
|
|
_selectedTeamId = null;
|
|
_selectedTeamName = 'Selecionar Equipa';
|
|
_selectedTeamLogo = null;
|
|
_teamWins = 0;
|
|
_teamLosses = 0;
|
|
_teamDraws = 0;
|
|
});
|
|
await _clearSelectedTeamFromPreferences();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Erro ao carregar equipa do Supabase: $e");
|
|
}
|
|
}
|
|
|
|
Future<void> _saveSelectedTeam() async {
|
|
await _saveToSharedPreferences();
|
|
|
|
final userId = _supabase.auth.currentUser?.id;
|
|
if (userId != null && _selectedTeamId != null) {
|
|
try {
|
|
await _supabase.from('profiles').upsert({
|
|
'id': userId,
|
|
'selected_team_id': _selectedTeamId,
|
|
});
|
|
} catch (e) {
|
|
debugPrint("Erro ao guardar equipa no Supabase: $e");
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _saveToSharedPreferences() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
if (_selectedTeamId != null) {
|
|
await prefs.setString(_prefsKey('last_team_id'), _selectedTeamId!);
|
|
await prefs.setString(_prefsKey('last_team_name'), _selectedTeamName);
|
|
if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) {
|
|
await prefs.setString(_prefsKey('last_team_logo'), _selectedTeamLogo!);
|
|
} else {
|
|
await prefs.remove(_prefsKey('last_team_logo'));
|
|
}
|
|
await prefs.setInt(_prefsKey('last_team_wins'), _teamWins);
|
|
await prefs.setInt(_prefsKey('last_team_losses'), _teamLosses);
|
|
await prefs.setInt(_prefsKey('last_team_draws'), _teamDraws);
|
|
}
|
|
}
|
|
|
|
Future<void> _clearSelectedTeamFromPreferences() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove(_prefsKey('last_team_id'));
|
|
await prefs.remove(_prefsKey('last_team_name'));
|
|
await prefs.remove(_prefsKey('last_team_logo'));
|
|
await prefs.remove(_prefsKey('last_team_wins'));
|
|
await prefs.remove(_prefsKey('last_team_losses'));
|
|
await prefs.remove(_prefsKey('last_team_draws'));
|
|
}
|
|
|
|
Future<void> _loadUserAvatar() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final savedUrl = prefs.getString(_prefsKey('meu_avatar_guardado'));
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
if (savedUrl != null) _avatarUrl = savedUrl;
|
|
_isMemoryLoaded = true;
|
|
});
|
|
}
|
|
|
|
final userId = _supabase.auth.currentUser?.id;
|
|
if (userId == null) return;
|
|
|
|
try {
|
|
final data = await _supabase
|
|
.from('profiles')
|
|
.select('avatar_url')
|
|
.eq('id', userId)
|
|
.maybeSingle();
|
|
if (mounted && data != null && data['avatar_url'] != null) {
|
|
final urlDoSupabase = data['avatar_url'];
|
|
if (urlDoSupabase != savedUrl) {
|
|
await prefs.setString(_prefsKey('meu_avatar_guardado'), urlDoSupabase);
|
|
setState(() {
|
|
_avatarUrl = urlDoSupabase;
|
|
});
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Erro ao carregar avatar na Home: $e");
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// A StatusPage recebe a equipa selecionada diretamente como parâmetro.
|
|
// Quando _selectedTeamId muda aqui, o didUpdateWidget da StatusPage
|
|
// atualiza automaticamente — sem precisar de ValueKey nem rebuild forçado.
|
|
final List<Widget> pages = [
|
|
_buildHomeContent(context),
|
|
const GamePage(),
|
|
const TeamsPage(),
|
|
StatusPage(
|
|
initialTeamId: _selectedTeamId,
|
|
initialTeamName: _selectedTeamName,
|
|
initialTeamLogo: _selectedTeamLogo,
|
|
),
|
|
];
|
|
|
|
return Scaffold(
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
appBar: AppBar(
|
|
title: Text('PlayMaker',
|
|
style: TextStyle(
|
|
fontSize: 20 * context.sf, fontWeight: FontWeight.bold)),
|
|
backgroundColor: AppTheme.primaryRed,
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
leading: Padding(
|
|
padding: EdgeInsets.all(10.0 * context.sf),
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(100),
|
|
onTap: () async {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const SettingsScreen()),
|
|
);
|
|
_loadUserAvatar();
|
|
},
|
|
child: !_isMemoryLoaded
|
|
? CircleAvatar(
|
|
backgroundColor: Colors.white.withOpacity(0.2))
|
|
: _avatarUrl != null && _avatarUrl!.isNotEmpty
|
|
? CachedNetworkImage(
|
|
imageUrl: _avatarUrl!,
|
|
fadeInDuration: Duration.zero,
|
|
imageBuilder: (context, imageProvider) => CircleAvatar(
|
|
backgroundColor: Colors.white.withOpacity(0.2),
|
|
backgroundImage: imageProvider,
|
|
),
|
|
placeholder: (context, url) => CircleAvatar(
|
|
backgroundColor: Colors.white.withOpacity(0.2)),
|
|
errorWidget: (context, url, error) => CircleAvatar(
|
|
backgroundColor: Colors.white.withOpacity(0.2),
|
|
child: Icon(Icons.person,
|
|
color: Colors.white, size: 20 * context.sf),
|
|
),
|
|
)
|
|
: CircleAvatar(
|
|
backgroundColor: Colors.white.withOpacity(0.2),
|
|
child: Icon(Icons.person,
|
|
color: Colors.white, size: 20 * context.sf),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
body: IndexedStack(index: _selectedIndex, children: pages),
|
|
bottomNavigationBar: NavigationBar(
|
|
selectedIndex: _selectedIndex,
|
|
onDestinationSelected: (index) {
|
|
setState(() => _selectedIndex = index);
|
|
if (index == 0) {
|
|
_loadSelectedTeam();
|
|
}
|
|
},
|
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
|
|
elevation: 1,
|
|
height: 70 * (context.sf < 1.2 ? context.sf : 1.2),
|
|
destinations: const [
|
|
NavigationDestination(
|
|
icon: Icon(Icons.home_outlined),
|
|
selectedIcon: Icon(Icons.home_filled),
|
|
label: 'Home'),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.sports_soccer_outlined),
|
|
selectedIcon: Icon(Icons.sports_soccer),
|
|
label: 'Jogo'),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.people_outline),
|
|
selectedIcon: Icon(Icons.people),
|
|
label: 'Equipas'),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.insights_outlined),
|
|
selectedIcon: Icon(Icons.insights),
|
|
label: 'Status'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showTeamSelector(BuildContext context) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius:
|
|
BorderRadius.vertical(top: Radius.circular(20 * context.sf)),
|
|
),
|
|
builder: (context) {
|
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
|
stream: _teamController.teamsStream,
|
|
builder: (context, snapshot) {
|
|
if (!snapshot.hasData &&
|
|
snapshot.connectionState == ConnectionState.waiting) {
|
|
return const SizedBox(
|
|
height: 200,
|
|
child: Center(child: CircularProgressIndicator()));
|
|
}
|
|
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
|
return SizedBox(
|
|
height: 200 * context.sf,
|
|
child: Center(
|
|
child: Text("Nenhuma equipa criada.",
|
|
style: TextStyle(
|
|
color:
|
|
Theme.of(context).colorScheme.onSurface))),
|
|
);
|
|
}
|
|
|
|
final teams = snapshot.data!;
|
|
return ListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: teams.length,
|
|
itemBuilder: (context, index) {
|
|
final team = teams[index];
|
|
final String? logoUrl = team['image_url'];
|
|
|
|
return ListTile(
|
|
leading: ClipOval(
|
|
child: Container(
|
|
width: 36 * context.sf,
|
|
height: 36 * context.sf,
|
|
color: AppTheme.primaryRed.withOpacity(0.1),
|
|
child: (logoUrl != null && logoUrl.isNotEmpty)
|
|
? CachedNetworkImage(
|
|
imageUrl: logoUrl,
|
|
fit: BoxFit.cover,
|
|
placeholder: (context, url) => Icon(
|
|
Icons.shield,
|
|
color: AppTheme.primaryRed,
|
|
size: 20 * context.sf),
|
|
errorWidget: (context, url, error) => Icon(
|
|
Icons.shield,
|
|
color: AppTheme.primaryRed,
|
|
size: 20 * context.sf),
|
|
)
|
|
: Icon(Icons.shield,
|
|
color: AppTheme.primaryRed,
|
|
size: 20 * context.sf),
|
|
),
|
|
),
|
|
title: Text(
|
|
team['name'] ?? 'Sem Nome',
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
onTap: () async {
|
|
setState(() {
|
|
_selectedTeamId = team['id'].toString();
|
|
_selectedTeamName = team['name'] ?? 'Desconhecido';
|
|
_selectedTeamLogo = logoUrl;
|
|
_teamWins = int.tryParse(
|
|
team['wins']?.toString() ?? '0') ??
|
|
0;
|
|
_teamLosses = int.tryParse(
|
|
team['losses']?.toString() ?? '0') ??
|
|
0;
|
|
_teamDraws = int.tryParse(
|
|
team['draws']?.toString() ?? '0') ??
|
|
0;
|
|
});
|
|
|
|
await _saveSelectedTeam();
|
|
if (context.mounted) Navigator.pop(context);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildHomeContent(BuildContext context) {
|
|
final double wScreen = MediaQuery.of(context).size.width;
|
|
final double cardHeight = wScreen * 0.5;
|
|
final textColor = Theme.of(context).colorScheme.onSurface;
|
|
|
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
|
stream: _selectedTeamId != null
|
|
? _supabase
|
|
.from('player_stats_with_names')
|
|
.stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!)
|
|
: const Stream.empty(),
|
|
builder: (context, snapshot) {
|
|
Map<String, dynamic> leaders =
|
|
_calculateLeaders(snapshot.data ?? []);
|
|
|
|
return SingleChildScrollView(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 22.0 * context.sf,
|
|
vertical: 16.0 * context.sf),
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final bool isWide = constraints.maxWidth >= 1100;
|
|
final double effectiveCardHeight =
|
|
isWide ? 280 * context.sf : cardHeight;
|
|
|
|
Widget statsSection = Column(
|
|
children: [
|
|
SizedBox(
|
|
height: effectiveCardHeight,
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStatCard(
|
|
context: context,
|
|
title: 'Mais Pontos',
|
|
playerName: leaders['pts_name'],
|
|
statValue: leaders['pts_val'].toString(),
|
|
statLabel: 'TOTAL',
|
|
color: AppTheme.statPtsBg,
|
|
isHighlighted: true)),
|
|
SizedBox(width: 12 * context.sf),
|
|
Expanded(
|
|
child: _buildStatCard(
|
|
context: context,
|
|
title: 'Assistências',
|
|
playerName: leaders['ast_name'],
|
|
statValue: leaders['ast_val'].toString(),
|
|
statLabel: 'TOTAL',
|
|
color: AppTheme.statAstBg)),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 12 * context.sf),
|
|
SizedBox(
|
|
height: effectiveCardHeight,
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStatCard(
|
|
context: context,
|
|
title: 'Rebotes',
|
|
playerName: leaders['rbs_name'],
|
|
statValue: leaders['rbs_val'].toString(),
|
|
statLabel: 'TOTAL',
|
|
color: AppTheme.statRebBg)),
|
|
SizedBox(width: 12 * context.sf),
|
|
Expanded(
|
|
child: PieChartCard(
|
|
victories: _teamWins,
|
|
defeats: _teamLosses,
|
|
draws: _teamDraws,
|
|
title: 'DESEMPENHO',
|
|
subtitle: 'Temporada',
|
|
backgroundColor: AppTheme.statPieBg,
|
|
sf: context.sf)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
Widget historySection = Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Histórico de Jogos',
|
|
style: TextStyle(
|
|
fontSize: 20 * context.sf,
|
|
fontWeight: FontWeight.bold,
|
|
color: textColor)),
|
|
SizedBox(height: 16 * context.sf),
|
|
_selectedTeamName == "Selecionar Equipa"
|
|
? Container(
|
|
width: double.infinity,
|
|
padding: EdgeInsets.all(24.0 * context.sf),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardTheme.color ??
|
|
Colors.white,
|
|
borderRadius:
|
|
BorderRadius.circular(16 * context.sf),
|
|
border: Border.all(
|
|
color: Colors.grey.withOpacity(0.1)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4))
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(18 * context.sf),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.primaryRed
|
|
.withOpacity(0.08),
|
|
shape: BoxShape.circle),
|
|
child: Icon(Icons.shield_outlined,
|
|
color: AppTheme.primaryRed,
|
|
size: 42 * context.sf),
|
|
),
|
|
SizedBox(height: 20 * context.sf),
|
|
Text("Nenhuma Equipa Ativa",
|
|
style: TextStyle(
|
|
fontSize: 18 * context.sf,
|
|
fontWeight: FontWeight.bold,
|
|
color: textColor)),
|
|
SizedBox(height: 8 * context.sf),
|
|
Text(
|
|
"Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 13 * context.sf,
|
|
color: Colors.grey.shade600,
|
|
height: 1.4),
|
|
),
|
|
SizedBox(height: 24 * context.sf),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 48 * context.sf,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => _showTeamSelector(context),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppTheme.primaryRed,
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(
|
|
10 * context.sf)),
|
|
),
|
|
icon: Icon(Icons.touch_app,
|
|
size: 20 * context.sf),
|
|
label: Text("Selecionar Agora",
|
|
style: TextStyle(
|
|
fontSize: 15 * context.sf,
|
|
fontWeight: FontWeight.bold)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: StreamBuilder<List<Map<String, dynamic>>>(
|
|
stream: _supabase
|
|
.from('games')
|
|
.stream(primaryKey: ['id'])
|
|
.order('game_date', ascending: false),
|
|
builder: (context, gameSnapshot) {
|
|
if (gameSnapshot.hasError) {
|
|
return Text(
|
|
"Erro: ${gameSnapshot.error}",
|
|
style: const TextStyle(
|
|
color: Colors.red));
|
|
}
|
|
if (!gameSnapshot.hasData &&
|
|
gameSnapshot.connectionState ==
|
|
ConnectionState.waiting) {
|
|
return const Center(
|
|
child: CircularProgressIndicator());
|
|
}
|
|
|
|
final todosOsJogos = gameSnapshot.data ?? [];
|
|
final gamesList = todosOsJogos.where((game) {
|
|
String myT =
|
|
game['my_team']?.toString() ?? '';
|
|
String oppT =
|
|
game['opponent_team']?.toString() ?? '';
|
|
String status =
|
|
game['status']?.toString() ?? '';
|
|
return (myT == _selectedTeamName ||
|
|
oppT == _selectedTeamName) &&
|
|
status == 'Terminado';
|
|
}).take(3).toList();
|
|
|
|
if (gamesList.isEmpty) {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: EdgeInsets.all(20 * context.sf),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardTheme.color,
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
alignment: Alignment.center,
|
|
child: const Text(
|
|
"Ainda não há jogos terminados.",
|
|
style: TextStyle(color: Colors.grey)),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
children: gamesList.map((game) {
|
|
String dbMyTeam =
|
|
game['my_team']?.toString() ?? '';
|
|
String dbOppTeam =
|
|
game['opponent_team']?.toString() ?? '';
|
|
int dbMyScore = int.tryParse(
|
|
game['my_score']?.toString() ??
|
|
'0') ??
|
|
0;
|
|
int dbOppScore = int.tryParse(
|
|
game['opponent_score']
|
|
?.toString() ??
|
|
'0') ??
|
|
0;
|
|
String opponent;
|
|
int myScore;
|
|
int oppScore;
|
|
|
|
if (dbMyTeam == _selectedTeamName) {
|
|
opponent = dbOppTeam;
|
|
myScore = dbMyScore;
|
|
oppScore = dbOppScore;
|
|
} else {
|
|
opponent = dbMyTeam;
|
|
myScore = dbOppScore;
|
|
oppScore = dbMyScore;
|
|
}
|
|
|
|
String rawDate =
|
|
game['game_date']?.toString() ?? '---';
|
|
String date = rawDate.length >= 10
|
|
? rawDate.substring(0, 10)
|
|
: rawDate;
|
|
String result = myScore > oppScore
|
|
? 'V'
|
|
: (myScore < oppScore ? 'D' : 'E');
|
|
|
|
return _buildGameHistoryCard(
|
|
context: context,
|
|
opponent: opponent,
|
|
result: result,
|
|
myScore: myScore,
|
|
oppScore: oppScore,
|
|
date: date,
|
|
topPts:
|
|
game['top_pts_name'] ?? '---',
|
|
topAst:
|
|
game['top_ast_name'] ?? '---',
|
|
topRbs:
|
|
game['top_rbs_name'] ?? '---',
|
|
topDef:
|
|
game['top_def_name'] ?? '---',
|
|
mvp: game['mvp_name'] ?? '---',
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
|
|
Widget mainContent = Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
InkWell(
|
|
onTap: () => _showTeamSelector(context),
|
|
child: Container(
|
|
padding: EdgeInsets.all(12 * context.sf),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardTheme.color,
|
|
borderRadius:
|
|
BorderRadius.circular(15 * context.sf),
|
|
border:
|
|
Border.all(color: Colors.grey.withOpacity(0.2)),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(children: [
|
|
(_selectedTeamLogo != null &&
|
|
_selectedTeamLogo!.isNotEmpty)
|
|
? ClipOval(
|
|
child: CachedNetworkImage(
|
|
imageUrl: _selectedTeamLogo!,
|
|
width: 24 * context.sf,
|
|
height: 24 * context.sf,
|
|
fit: BoxFit.cover,
|
|
placeholder: (context, url) => Icon(
|
|
Icons.shield,
|
|
color: AppTheme.primaryRed,
|
|
size: 24 * context.sf),
|
|
errorWidget:
|
|
(context, url, error) => Icon(
|
|
Icons.shield,
|
|
color: AppTheme.primaryRed,
|
|
size: 24 * context.sf),
|
|
),
|
|
)
|
|
: Icon(Icons.shield,
|
|
color: AppTheme.primaryRed,
|
|
size: 24 * context.sf),
|
|
SizedBox(width: 10 * context.sf),
|
|
Text(_selectedTeamName,
|
|
style: TextStyle(
|
|
fontSize: 16 * context.sf,
|
|
fontWeight: FontWeight.bold,
|
|
color: textColor)),
|
|
]),
|
|
Icon(Icons.arrow_drop_down, color: textColor),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 20 * context.sf),
|
|
if (!isWide) ...[
|
|
statsSection,
|
|
SizedBox(height: 40 * context.sf),
|
|
historySection,
|
|
]
|
|
],
|
|
);
|
|
|
|
if (isWide) {
|
|
mainContent = Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(flex: 5, child: statsSection),
|
|
SizedBox(width: 20 * context.sf),
|
|
Expanded(flex: 6, child: historySection),
|
|
],
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
mainContent,
|
|
SizedBox(height: 20 * context.sf),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> _calculateLeaders(List<Map<String, dynamic>> data) {
|
|
Map<String, int> ptsMap = {};
|
|
Map<String, int> astMap = {};
|
|
Map<String, int> rbsMap = {};
|
|
Map<String, String> namesMap = {};
|
|
|
|
for (var row in data) {
|
|
String pid = row['member_id']?.toString() ?? "unknown";
|
|
namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido";
|
|
ptsMap[pid] = (ptsMap[pid] ?? 0) +
|
|
(int.tryParse(row['pts']?.toString() ?? '0') ?? 0);
|
|
astMap[pid] = (astMap[pid] ?? 0) +
|
|
(int.tryParse(row['ast']?.toString() ?? '0') ?? 0);
|
|
rbsMap[pid] = (rbsMap[pid] ?? 0) +
|
|
(int.tryParse(row['rbs']?.toString() ?? '0') ?? 0);
|
|
}
|
|
|
|
if (ptsMap.isEmpty) {
|
|
return {
|
|
'pts_name': '---',
|
|
'pts_val': 0,
|
|
'ast_name': '---',
|
|
'ast_val': 0,
|
|
'rbs_name': '---',
|
|
'rbs_val': 0,
|
|
};
|
|
}
|
|
|
|
String getBest(Map<String, int> map) {
|
|
if (map.isEmpty) return '---';
|
|
return namesMap[
|
|
map.entries.reduce((a, b) => a.value > b.value ? a : b).key] ??
|
|
'---';
|
|
}
|
|
|
|
int getBestVal(Map<String, int> map) {
|
|
if (map.isEmpty) return 0;
|
|
return map.values.reduce((a, b) => a > b ? a : b);
|
|
}
|
|
|
|
return {
|
|
'pts_name': getBest(ptsMap),
|
|
'pts_val': getBestVal(ptsMap),
|
|
'ast_name': getBest(astMap),
|
|
'ast_val': getBestVal(astMap),
|
|
'rbs_name': getBest(rbsMap),
|
|
'rbs_val': getBestVal(rbsMap),
|
|
};
|
|
}
|
|
|
|
Widget _buildStatCard({
|
|
required BuildContext context,
|
|
required String title,
|
|
required String playerName,
|
|
required String statValue,
|
|
required String statLabel,
|
|
required Color color,
|
|
bool isHighlighted = false,
|
|
}) {
|
|
return Card(
|
|
elevation: 4,
|
|
margin: EdgeInsets.zero,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
side: isHighlighted
|
|
? const BorderSide(color: AppTheme.warningAmber, width: 2)
|
|
: BorderSide.none,
|
|
),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(14),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [color.withOpacity(0.9), color],
|
|
),
|
|
),
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final double ch = constraints.maxHeight;
|
|
final double cw = constraints.maxWidth;
|
|
return Padding(
|
|
padding: EdgeInsets.all(cw * 0.06),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title.toUpperCase(),
|
|
style: TextStyle(
|
|
fontSize: ch * 0.06,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white70),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
SizedBox(height: ch * 0.011),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(playerName,
|
|
style: TextStyle(
|
|
fontSize: ch * 0.08,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white)),
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Center(
|
|
child: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: Text(
|
|
statValue,
|
|
style: TextStyle(
|
|
fontSize: ch * 0.18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
height: 1.0),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: ch * 0.015),
|
|
Center(
|
|
child: Text(statLabel,
|
|
style: TextStyle(
|
|
fontSize: ch * 0.05, color: Colors.white70))),
|
|
const Spacer(),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: EdgeInsets.symmetric(vertical: ch * 0.035),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white24,
|
|
borderRadius: BorderRadius.circular(ch * 0.03)),
|
|
child: Center(
|
|
child: Text('DETALHES',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: ch * 0.05,
|
|
fontWeight: FontWeight.bold))),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildGameHistoryCard({
|
|
required BuildContext context,
|
|
required String opponent,
|
|
required String result,
|
|
required int myScore,
|
|
required int oppScore,
|
|
required String date,
|
|
required String topPts,
|
|
required String topAst,
|
|
required String topRbs,
|
|
required String topDef,
|
|
required String mvp,
|
|
}) {
|
|
bool isWin = result == 'V';
|
|
bool isDraw = result == 'E';
|
|
Color statusColor = isWin
|
|
? AppTheme.successGreen
|
|
: (isDraw ? AppTheme.warningAmber : AppTheme.oppTeamRed);
|
|
final bgColor = Theme.of(context).cardTheme.color;
|
|
final textColor = Theme.of(context).colorScheme.onSurface;
|
|
|
|
return Container(
|
|
margin: EdgeInsets.only(bottom: 14 * context.sf),
|
|
decoration: BoxDecoration(
|
|
color: bgColor,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4))
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.all(14 * context.sf),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 36 * context.sf,
|
|
height: 36 * context.sf,
|
|
decoration: BoxDecoration(
|
|
color: statusColor.withOpacity(0.15),
|
|
shape: BoxShape.circle),
|
|
child: Center(
|
|
child: Text(result,
|
|
style: TextStyle(
|
|
color: statusColor,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16 * context.sf))),
|
|
),
|
|
SizedBox(width: 14 * context.sf),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(date,
|
|
style: TextStyle(
|
|
fontSize: 11 * context.sf,
|
|
color: Colors.grey,
|
|
fontWeight: FontWeight.w600)),
|
|
SizedBox(height: 6 * context.sf),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
_selectedTeamName == "Selecionar Equipa"
|
|
? "Minha Equipa"
|
|
: _selectedTeamName,
|
|
style: TextStyle(
|
|
fontSize: 14 * context.sf,
|
|
fontWeight: FontWeight.bold,
|
|
color: textColor),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 8 * context.sf),
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 8 * context.sf,
|
|
vertical: 4 * context.sf),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onSurface
|
|
.withOpacity(0.05),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
'$myScore - $oppScore',
|
|
style: TextStyle(
|
|
fontSize: 15 * context.sf,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 1.5,
|
|
color: textColor),
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
opponent,
|
|
style: TextStyle(
|
|
fontSize: 14 * context.sf,
|
|
fontWeight: FontWeight.bold,
|
|
color: textColor),
|
|
textAlign: TextAlign.right,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Divider(
|
|
height: 1,
|
|
color: Colors.grey.withOpacity(0.1),
|
|
thickness: 1.5),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 16 * context.sf, vertical: 12 * context.sf),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
borderRadius: const BorderRadius.only(
|
|
bottomLeft: Radius.circular(16),
|
|
bottomRight: Radius.circular(16)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(children: [
|
|
Expanded(
|
|
child: _buildGridStatRow(context,
|
|
Icons.workspace_premium, Colors.amber.shade700,
|
|
"MVP", mvp, isMvp: true)),
|
|
Expanded(
|
|
child: _buildGridStatRow(context, Icons.shield,
|
|
Colors.deepOrange.shade700, "Defesa", topDef)),
|
|
]),
|
|
SizedBox(height: 8 * context.sf),
|
|
Row(children: [
|
|
Expanded(
|
|
child: _buildGridStatRow(context, Icons.bolt,
|
|
Colors.blue.shade700, "Pontos", topPts)),
|
|
Expanded(
|
|
child: _buildGridStatRow(context, Icons.trending_up,
|
|
Colors.purple.shade700, "Rebotes", topRbs)),
|
|
]),
|
|
SizedBox(height: 8 * context.sf),
|
|
Row(children: [
|
|
Expanded(
|
|
child: _buildGridStatRow(context, Icons.star,
|
|
Colors.green.shade700, "Assists", topAst)),
|
|
const Expanded(child: SizedBox()),
|
|
]),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildGridStatRow(BuildContext context, IconData icon, Color color,
|
|
String label, String value,
|
|
{bool isMvp = false}) {
|
|
return Row(
|
|
children: [
|
|
Icon(icon, size: 14 * context.sf, color: color),
|
|
SizedBox(width: 4 * context.sf),
|
|
Text('$label: ',
|
|
style: TextStyle(
|
|
fontSize: 11 * context.sf,
|
|
color: Colors.grey,
|
|
fontWeight: FontWeight.bold)),
|
|
Expanded(
|
|
child: Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: 11 * context.sf,
|
|
color: isMvp
|
|
? AppTheme.warningAmber
|
|
: Theme.of(context).colorScheme.onSurface,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
} |