melhorar o sensor de calor

This commit is contained in:
2026-03-13 18:08:15 +00:00
parent cae3bbfe3b
commit 0369b5376c
10 changed files with 1053 additions and 926 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
@@ -6,7 +7,7 @@ class ShotRecord {
final double relativeX; final double relativeX;
final double relativeY; final double relativeY;
final bool isMake; final bool isMake;
final String playerName; // Bónus: Agora guardamos quem foi o jogador! final String playerName;
ShotRecord({ ShotRecord({
required this.relativeX, required this.relativeX,
@@ -31,8 +32,6 @@ class PlacarController {
bool isLoading = true; bool isLoading = true;
bool isSaving = false; bool isSaving = false;
// 👇 TRINCO DE SEGURANÇA: Evita contar vitórias duas vezes se clicares no Guardar repetidamente!
bool gameWasAlreadyFinished = false; bool gameWasAlreadyFinished = false;
int myScore = 0; int myScore = 0;
@@ -67,35 +66,31 @@ class PlacarController {
Timer? timer; Timer? timer;
bool isRunning = false; bool isRunning = false;
// --- 🔄 CARREGAMENTO COMPLETO (DADOS REAIS + ESTATÍSTICAS SALVAS) --- // OS TEUS NÚMEROS DE OURO DO TABLET
bool isCalibrating = false;
double hoopBaseX = 0.000;
double arcRadius = 0.500;
double cornerY = 0.443;
Future<void> loadPlayers() async { Future<void> loadPlayers() async {
final supabase = Supabase.instance.client; final supabase = Supabase.instance.client;
try { try {
await Future.delayed(const Duration(milliseconds: 1500)); await Future.delayed(const Duration(milliseconds: 1500));
myCourt.clear(); myCourt.clear(); myBench.clear(); oppCourt.clear(); oppBench.clear();
myBench.clear(); playerStats.clear(); playerNumbers.clear(); playerDbIds.clear();
oppCourt.clear(); myFouls = 0; opponentFouls = 0;
oppBench.clear();
playerStats.clear();
playerNumbers.clear();
playerDbIds.clear();
myFouls = 0;
opponentFouls = 0;
final gameResponse = await supabase.from('games').select().eq('id', gameId).single(); final gameResponse = await supabase.from('games').select().eq('id', gameId).single();
myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0; myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0;
opponentScore = int.tryParse(gameResponse['opponent_score']?.toString() ?? '0') ?? 0; opponentScore = int.tryParse(gameResponse['opponent_score']?.toString() ?? '0') ?? 0;
int totalSeconds = int.tryParse(gameResponse['remaining_seconds']?.toString() ?? '600') ?? 600; int totalSeconds = int.tryParse(gameResponse['remaining_seconds']?.toString() ?? '600') ?? 600;
duration = Duration(seconds: totalSeconds); duration = Duration(seconds: totalSeconds);
myTimeoutsUsed = int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0; myTimeoutsUsed = int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0;
opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0; opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
currentQuarter = int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1; currentQuarter = int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1;
// 👇 Verifica se o jogo já tinha acabado noutra sessão
gameWasAlreadyFinished = gameResponse['status'] == 'Terminado'; gameWasAlreadyFinished = gameResponse['status'] == 'Terminado';
final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]); final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
@@ -115,17 +110,10 @@ class PlacarController {
for (int i = 0; i < myPlayers.length; i++) { for (int i = 0; i < myPlayers.length; i++) {
String dbId = myPlayers[i]['id'].toString(); String dbId = myPlayers[i]['id'].toString();
String name = myPlayers[i]['name'].toString(); String name = myPlayers[i]['name'].toString();
_registerPlayer(name: name, number: myPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: true, isCourt: i < 5); _registerPlayer(name: name, number: myPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: true, isCourt: i < 5);
if (savedStats.containsKey(dbId)) { if (savedStats.containsKey(dbId)) {
var s = savedStats[dbId]; var s = savedStats[dbId];
playerStats[name] = { playerStats[name] = { "pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0, "stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0, "fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0, "ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0 };
"pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0,
"stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0,
"fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0,
"ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0,
};
myFouls += (s['fls'] as int? ?? 0); myFouls += (s['fls'] as int? ?? 0);
} }
} }
@@ -134,28 +122,28 @@ class PlacarController {
for (int i = 0; i < oppPlayers.length; i++) { for (int i = 0; i < oppPlayers.length; i++) {
String dbId = oppPlayers[i]['id'].toString(); String dbId = oppPlayers[i]['id'].toString();
String name = oppPlayers[i]['name'].toString(); String name = oppPlayers[i]['name'].toString();
_registerPlayer(name: name, number: oppPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: false, isCourt: i < 5); _registerPlayer(name: name, number: oppPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: false, isCourt: i < 5);
if (savedStats.containsKey(dbId)) { if (savedStats.containsKey(dbId)) {
var s = savedStats[dbId]; var s = savedStats[dbId];
playerStats[name] = { playerStats[name] = { "pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0, "stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0, "fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0, "ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0 };
"pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0,
"stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0,
"fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0,
"ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0,
};
opponentFouls += (s['fls'] as int? ?? 0); opponentFouls += (s['fls'] as int? ?? 0);
} }
} }
_padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false); _padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false);
// Carregar Shots salvos para o HeatMap
final shotsResponse = await supabase.from('game_shots').select().eq('game_id', gameId);
matchShots = (shotsResponse as List).map((s) => ShotRecord(
relativeX: (s['relative_x'] as num).toDouble(),
relativeY: (s['relative_y'] as num).toDouble(),
isMake: s['is_make'] as bool,
playerName: s['player_name'],
)).toList();
isLoading = false; isLoading = false;
onUpdate(); onUpdate();
} catch (e) { } catch (e) {
debugPrint("Erro ao retomar jogo: $e"); debugPrint("Erro ao retomar jogo: $e");
_padTeam(myCourt, myBench, "Falha", isMyTeam: true);
_padTeam(oppCourt, oppBench, "Falha Opp", isMyTeam: false);
isLoading = false; isLoading = false;
onUpdate(); onUpdate();
} }
@@ -165,17 +153,9 @@ class PlacarController {
if (playerNumbers.containsKey(name)) name = "$name (Opp)"; if (playerNumbers.containsKey(name)) name = "$name (Opp)";
playerNumbers[name] = number; playerNumbers[name] = number;
if (dbId != null) playerDbIds[name] = dbId; if (dbId != null) playerDbIds[name] = dbId;
playerStats[name] = { "pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0, "ftm": 0, "fta": 0, "orb": 0, "drb": 0 };
playerStats[name] = { if (isMyTeam) { if (isCourt) myCourt.add(name); else myBench.add(name); }
"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, else { if (isCourt) oppCourt.add(name); else oppBench.add(name); }
"fls": 0, "fgm": 0, "fga": 0, "ftm": 0, "fta": 0, "orb": 0, "drb": 0
};
if (isMyTeam) {
if (isCourt) myCourt.add(name); else myBench.add(name);
} else {
if (isCourt) oppCourt.add(name); else oppBench.add(name);
}
} }
void _padTeam(List<String> court, List<String> bench, String prefix, {required bool isMyTeam}) { void _padTeam(List<String> court, List<String> bench, String prefix, {required bool isMyTeam}) {
@@ -194,17 +174,12 @@ class PlacarController {
} else { } else {
timer.cancel(); timer.cancel();
isRunning = false; isRunning = false;
if (currentQuarter < 4) { if (currentQuarter < 4) {
currentQuarter++; currentQuarter++;
duration = const Duration(minutes: 10); duration = const Duration(minutes: 10);
myFouls = 0; myFouls = 0; opponentFouls = 0; myTimeoutsUsed = 0; opponentTimeoutsUsed = 0;
opponentFouls = 0; onUpdate();
myTimeoutsUsed = 0; }
opponentTimeoutsUsed = 0;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas e Timeouts resetados!'), backgroundColor: Colors.blue));
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO! Clica em Guardar para fechar a partida.'), backgroundColor: Colors.red));
}
} }
onUpdate(); onUpdate();
}); });
@@ -214,11 +189,8 @@ class PlacarController {
} }
void useTimeout(bool isOpponent) { void useTimeout(bool isOpponent) {
if (isOpponent) { if (isOpponent) { if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++; }
if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++; else { if (myTimeoutsUsed < 3) myTimeoutsUsed++; }
} else {
if (myTimeoutsUsed < 3) myTimeoutsUsed++;
}
isRunning = false; isRunning = false;
timer?.cancel(); timer?.cancel();
onUpdate(); onUpdate();
@@ -254,7 +226,6 @@ class PlacarController {
myCourt[courtIndex] = benchPlayer; myCourt[courtIndex] = benchPlayer;
myBench[benchIndex] = courtPlayerName; myBench[benchIndex] = courtPlayerName;
showMyBench = false; showMyBench = false;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer')));
} }
if (action.startsWith("bench_opp_") && isOpponent) { if (action.startsWith("bench_opp_") && isOpponent) {
String benchPlayer = action.replaceAll("bench_opp_", ""); String benchPlayer = action.replaceAll("bench_opp_", "");
@@ -264,41 +235,36 @@ class PlacarController {
oppCourt[courtIndex] = benchPlayer; oppCourt[courtIndex] = benchPlayer;
oppBench[benchIndex] = courtPlayerName; oppBench[benchIndex] = courtPlayerName;
showOppBench = false; showOppBench = false;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer')));
} }
onUpdate(); onUpdate();
} }
void registerShotLocation(BuildContext context, Offset position, Size size) { // ==============================================================
// 🎯 REGISTO DO TOQUE (INTELIGENTE E SILENCIOSO)
// ==============================================================
void registerShotLocation(BuildContext context, Offset position, Size size) {
if (pendingAction == null || pendingPlayer == null) return; if (pendingAction == null || pendingPlayer == null) return;
bool isOpponent = pendingPlayer!.startsWith("player_opp_");
bool is3Pt = pendingAction!.contains("_3"); bool is3Pt = pendingAction!.contains("_3");
bool is2Pt = pendingAction!.contains("_2"); bool is2Pt = pendingAction!.contains("_2");
if (is3Pt || is2Pt) { if (is3Pt || is2Pt) {
bool isValid = _validateShotZone(position, size, is3Pt); bool isInside2Pts = _validateShotZone(position, size, isOpponent);
if (!isValid) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 Local incompatível com a pontuação.'), backgroundColor: Colors.red, duration: Duration(seconds: 2))); // Bloqueio silencioso (sem notificações chamas)
if ((is2Pt && !isInside2Pts) || (is3Pt && isInside2Pts)) {
cancelShotLocation();
return; return;
} }
} }
bool isMake = pendingAction!.startsWith("add_pts_"); bool isMake = pendingAction!.startsWith("add_pts_");
// 👇 A MÁGICA DAS COORDENADAS RELATIVAS (0.0 a 1.0) 👇
double relX = position.dx / size.width; double relX = position.dx / size.width;
double relY = position.dy / size.height; double relY = position.dy / size.height;
// Extrai só o nome do jogador
String name = pendingPlayer!.replaceAll("player_my_", "").replaceAll("player_opp_", ""); String name = pendingPlayer!.replaceAll("player_my_", "").replaceAll("player_opp_", "");
// Guarda na lista! matchShots.add(ShotRecord(relativeX: relX, relativeY: relY, isMake: isMake, playerName: name));
matchShots.add(ShotRecord(
relativeX: relX,
relativeY: relY,
isMake: isMake,
playerName: name
));
commitStat(pendingAction!, pendingPlayer!); commitStat(pendingAction!, pendingPlayer!);
isSelectingShotLocation = false; isSelectingShotLocation = false;
@@ -307,17 +273,36 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
onUpdate(); onUpdate();
} }
bool _validateShotZone(Offset pos, Size size, bool is3Pt) { // ==============================================================
double w = size.width; double h = size.height; // 📐 MATEMÁTICA PURA: LÓGICA DE MEIO-CAMPO ATACANTE (SOLUÇÃO DIVIDIDA)
Offset leftHoop = Offset(w * 0.12, h * 0.5); // ==============================================================
Offset rightHoop = Offset(w * 0.88, h * 0.5); bool _validateShotZone(Offset position, Size size, bool isOpponent) {
double threePointRadius = w * 0.28; double relX = position.dx / size.width;
Offset activeHoop = pos.dx < w / 2 ? leftHoop : rightHoop; double relY = position.dy / size.height;
double distanceToHoop = (pos - activeHoop).distance;
bool isCorner3 = (pos.dy < h * 0.15 || pos.dy > h * 0.85) && (pos.dx < w * 0.20 || pos.dx > w * 0.80);
if (is3Pt) return distanceToHoop >= threePointRadius || isCorner3; double hX = hoopBaseX;
else return distanceToHoop < threePointRadius && !isCorner3; double radius = arcRadius;
double cY = cornerY;
// A Minha Equipa defende na Esquerda (0.0), logo ataca o cesto da Direita (1.0)
// O Adversário defende na Direita (1.0), logo ataca o cesto da Esquerda (0.0)
double hoopX = isOpponent ? hX : (1.0 - hX);
double hoopY = 0.50;
double aspectRatio = size.width / size.height;
double distFromCenterY = (relY - hoopY).abs();
// Descobre se o toque foi feito na metade atacante daquela equipa
bool isAttackingHalf = isOpponent ? (relX < 0.5) : (relX > 0.5);
if (isAttackingHalf && distFromCenterY > cY) {
return false; // É 3 pontos (Zona dos Cantos)
} else {
double dx = (relX - hoopX) * aspectRatio;
double dy = (relY - hoopY);
double distanceToHoop = math.sqrt((dx * dx) + (dy * dy));
return distanceToHoop <= radius;
}
} }
void cancelShotLocation() { void cancelShotLocation() {
@@ -368,97 +353,63 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
} }
} }
// --- 💾 FUNÇÃO PARA GUARDAR DADOS NA BD ---
Future<void> saveGameStats(BuildContext context) async { Future<void> saveGameStats(BuildContext context) async {
final supabase = Supabase.instance.client; final supabase = Supabase.instance.client;
isSaving = true; isSaving = true;
onUpdate(); onUpdate();
try { try {
bool isGameFinishedNow = currentQuarter >= 4 && duration.inSeconds == 0; bool isGameFinishedNow = (currentQuarter >= 4 && duration.inSeconds == 0);
String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado'; String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado';
// 👇👇👇 0. CÉREBRO: CALCULAR OS LÍDERES E MVP DO JOGO 👇👇👇
String topPtsName = '---'; int maxPts = -1; String topPtsName = '---'; int maxPts = -1;
String topAstName = '---'; int maxAst = -1; String topAstName = '---'; int maxAst = -1;
String topRbsName = '---'; int maxRbs = -1; String topRbsName = '---'; int maxRbs = -1;
String topDefName = '---'; int maxDef = -1; String topDefName = '---'; int maxDef = -1;
String mvpName = '---'; int maxMvpScore = -1; String mvpName = '---'; int maxMvpScore = -1;
// Passa por todos os jogadores e calcula a matemática
playerStats.forEach((playerName, stats) { playerStats.forEach((playerName, stats) {
int pts = stats['pts'] ?? 0; int pts = stats['pts'] ?? 0;
int ast = stats['ast'] ?? 0; int ast = stats['ast'] ?? 0;
int rbs = stats['rbs'] ?? 0; int rbs = stats['rbs'] ?? 0;
int stl = stats['stl'] ?? 0; int stl = stats['stl'] ?? 0;
int blk = stats['blk'] ?? 0; int blk = stats['blk'] ?? 0;
int defScore = stl + blk;
int defScore = stl + blk; // Defesa: Roubos + Cortes int mvpScore = pts + ast + rbs + defScore;
int mvpScore = pts + ast + rbs + defScore; // Impacto Total (MVP)
// Compara com o máximo atual e substitui se for maior
if (pts > maxPts && pts > 0) { maxPts = pts; topPtsName = '$playerName ($pts)'; } if (pts > maxPts && pts > 0) { maxPts = pts; topPtsName = '$playerName ($pts)'; }
if (ast > maxAst && ast > 0) { maxAst = ast; topAstName = '$playerName ($ast)'; } if (ast > maxAst && ast > 0) { maxAst = ast; topAstName = '$playerName ($ast)'; }
if (rbs > maxRbs && rbs > 0) { maxRbs = rbs; topRbsName = '$playerName ($rbs)'; } if (rbs > maxRbs && rbs > 0) { maxRbs = rbs; topRbsName = '$playerName ($rbs)'; }
if (defScore > maxDef && defScore > 0) { maxDef = defScore; topDefName = '$playerName ($defScore)'; } if (defScore > maxDef && defScore > 0) { maxDef = defScore; topDefName = '$playerName ($defScore)'; }
if (mvpScore > maxMvpScore && mvpScore > 0) { maxMvpScore = mvpScore; mvpName = playerName; } // MVP não leva nº à frente, fica mais limpo if (mvpScore > maxMvpScore && mvpScore > 0) { maxMvpScore = mvpScore; mvpName = playerName; }
}); });
// 👆👆👆 FIM DO CÉREBRO 👆👆👆
// 1. Atualizar o Jogo na BD (Agora inclui os Reis da partida!)
await supabase.from('games').update({ await supabase.from('games').update({
'my_score': myScore, 'my_score': myScore, 'opponent_score': opponentScore, 'remaining_seconds': duration.inSeconds,
'opponent_score': opponentScore, 'my_timeouts': myTimeoutsUsed, 'opp_timeouts': opponentTimeoutsUsed, 'current_quarter': currentQuarter,
'remaining_seconds': duration.inSeconds, 'status': newStatus, 'top_pts_name': topPtsName, 'top_ast_name': topAstName, 'top_rbs_name': topRbsName,
'my_timeouts': myTimeoutsUsed, 'top_def_name': topDefName, 'mvp_name': mvpName,
'opp_timeouts': opponentTimeoutsUsed,
'current_quarter': currentQuarter,
'status': newStatus,
// ENVIA A MATEMÁTICA PARA A TUA BASE DE DADOS
'top_pts_name': topPtsName,
'top_ast_name': topAstName,
'top_rbs_name': topRbsName,
'top_def_name': topDefName,
'mvp_name': mvpName,
}).eq('id', gameId); }).eq('id', gameId);
// 2. LÓGICA DE VITÓRIAS, DERROTAS E EMPATES // Atualiza Vitórias/Derrotas se o jogo terminou
if (isGameFinishedNow && !gameWasAlreadyFinished && myTeamDbId != null && oppTeamDbId != null) { if (isGameFinishedNow && !gameWasAlreadyFinished && myTeamDbId != null && oppTeamDbId != null) {
final teamsData = await supabase.from('teams').select('id, wins, losses, draws').inFilter('id', [myTeamDbId, oppTeamDbId]); final teamsData = await supabase.from('teams').select('id, wins, losses, draws').inFilter('id', [myTeamDbId, oppTeamDbId]);
Map<String, dynamic> myTeamUpdate = {};
Map<String, dynamic> oppTeamUpdate = {};
for(var t in teamsData) { for(var t in teamsData) {
if(t['id'].toString() == myTeamDbId) myTeamUpdate = Map.from(t); if(t['id'].toString() == myTeamDbId) {
if(t['id'].toString() == oppTeamDbId) oppTeamUpdate = Map.from(t); int w = (t['wins'] ?? 0) + (myScore > opponentScore ? 1 : 0);
int l = (t['losses'] ?? 0) + (myScore < opponentScore ? 1 : 0);
int d = (t['draws'] ?? 0) + (myScore == opponentScore ? 1 : 0);
await supabase.from('teams').update({'wins': w, 'losses': l, 'draws': d}).eq('id', myTeamDbId!);
} else {
int w = (t['wins'] ?? 0) + (opponentScore > myScore ? 1 : 0);
int l = (t['losses'] ?? 0) + (opponentScore < myScore ? 1 : 0);
int d = (t['draws'] ?? 0) + (opponentScore == myScore ? 1 : 0);
await supabase.from('teams').update({'wins': w, 'losses': l, 'draws': d}).eq('id', oppTeamDbId!);
}
} }
if (myScore > opponentScore) {
myTeamUpdate['wins'] = (myTeamUpdate['wins'] ?? 0) + 1;
oppTeamUpdate['losses'] = (oppTeamUpdate['losses'] ?? 0) + 1;
} else if (myScore < opponentScore) {
myTeamUpdate['losses'] = (myTeamUpdate['losses'] ?? 0) + 1;
oppTeamUpdate['wins'] = (oppTeamUpdate['wins'] ?? 0) + 1;
} else {
myTeamUpdate['draws'] = (myTeamUpdate['draws'] ?? 0) + 1;
oppTeamUpdate['draws'] = (oppTeamUpdate['draws'] ?? 0) + 1;
}
await supabase.from('teams').update({
'wins': myTeamUpdate['wins'], 'losses': myTeamUpdate['losses'], 'draws': myTeamUpdate['draws']
}).eq('id', myTeamDbId!);
await supabase.from('teams').update({
'wins': oppTeamUpdate['wins'], 'losses': oppTeamUpdate['losses'], 'draws': oppTeamUpdate['draws']
}).eq('id', oppTeamDbId!);
gameWasAlreadyFinished = true; gameWasAlreadyFinished = true;
} }
// 3. Atualizar as Estatísticas dos Jogadores // Salvar Estatísticas Gerais
List<Map<String, dynamic>> batchStats = []; List<Map<String, dynamic>> batchStats = [];
playerStats.forEach((playerName, stats) { playerStats.forEach((playerName, stats) {
String? memberDbId = playerDbIds[playerName]; String? memberDbId = playerDbIds[playerName];
@@ -470,21 +421,32 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
}); });
} }
}); });
await supabase.from('player_stats').delete().eq('game_id', gameId); await supabase.from('player_stats').delete().eq('game_id', gameId);
if (batchStats.isNotEmpty) { if (batchStats.isNotEmpty) await supabase.from('player_stats').insert(batchStats);
await supabase.from('player_stats').insert(batchStats);
// ===============================================
// 🔥 GRAVAR COORDENADAS PARA O HEATMAP
// ===============================================
List<Map<String, dynamic>> shotsData = [];
for (var shot in matchShots) {
bool isMyTeamPlayer = myCourt.contains(shot.playerName) || myBench.contains(shot.playerName);
shotsData.add({
'game_id': gameId,
'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!,
'player_name': shot.playerName,
'relative_x': shot.relativeX,
'relative_y': shot.relativeY,
'is_make': shot.isMake,
});
} }
await supabase.from('game_shots').delete().eq('game_id', gameId);
if (shotsData.isNotEmpty) await supabase.from('game_shots').insert(shotsData);
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas e Resultados guardados com Sucesso!'), backgroundColor: Colors.green)); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Tudo guardado com Sucesso!'), backgroundColor: Colors.green));
} }
} catch (e) { } catch (e) {
debugPrint("Erro ao gravar estatísticas: $e"); if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao guardar: $e'), backgroundColor: Colors.red));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao guardar: $e'), backgroundColor: Colors.red));
}
} finally { } finally {
isSaving = false; isSaving = false;
onUpdate(); onUpdate();
@@ -494,4 +456,4 @@ void registerShotLocation(BuildContext context, Offset position, Size size) {
void dispose() { void dispose() {
timer?.cancel(); timer?.cancel();
} }
} }

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart'; import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart';
import 'dados_grafico.dart'; import 'dados_grafico.dart';
import 'dart:math' as math;
class PieChartCard extends StatefulWidget { class PieChartCard extends StatefulWidget {
final int victories; final int victories;
@@ -59,30 +60,25 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
Widget build(BuildContext context) { Widget build(BuildContext context) {
final data = PieChartData(victories: widget.victories, defeats: widget.defeats, draws: widget.draws); final data = PieChartData(victories: widget.victories, defeats: widget.defeats, draws: widget.draws);
return AnimatedBuilder( return AnimatedBuilder(
animation: _animation, animation: _animation,
builder: (context, child) { builder: (context, child) {
return Transform.scale( return Transform.scale(
// O scale pode passar de 1.0 (efeito back), mas a opacidade NÃO
scale: 0.95 + (_animation.value * 0.05), scale: 0.95 + (_animation.value * 0.05),
child: Opacity( child: Opacity(opacity: _animation.value.clamp(0.0, 1.0), child: child),
// 👇 AQUI ESTÁ A FIX: Garante que fica entre 0 e 1
opacity: _animation.value.clamp(0.0, 1.0),
child: child,
),
); );
}, },
child: Card( child: Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
elevation: 4, elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), shadowColor: Colors.black54,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: InkWell( child: InkWell(
onTap: widget.onTap, onTap: widget.onTap,
borderRadius: BorderRadius.circular(14),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14), color: const Color(0xFF1A222D),
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [widget.backgroundColor.withOpacity(0.9), widget.backgroundColor.withOpacity(0.7)]),
), ),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
@@ -90,86 +86,89 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
final double cw = constraints.maxWidth; final double cw = constraints.maxWidth;
return Padding( return Padding(
padding: EdgeInsets.all(cw * 0.06), padding: EdgeInsets.symmetric(horizontal: cw * 0.05, vertical: ch * 0.03),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 👇 TÍTULOS UM POUCO MAIS PRESENTES // --- CABEÇALHO ---
FittedBox( FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Text(widget.title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), letterSpacing: 1.0)), child: Text(widget.title.toUpperCase(),
), style: TextStyle(fontSize: ch * 0.045, fontWeight: FontWeight.bold, color: Colors.white70, letterSpacing: 1.2)),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(widget.subtitle, style: TextStyle(fontSize: ch * 0.07, fontWeight: FontWeight.bold, color: Colors.white)),
), ),
Text(widget.subtitle,
style: TextStyle(fontSize: ch * 0.055, fontWeight: FontWeight.bold, color: Colors.white)),
SizedBox(height: ch * 0.03), const Expanded(flex: 1, child: SizedBox()),
// MEIO (GRÁFICO + ESTATÍSTICAS) // --- MIOLO (GRÁFICO + STATS GIGANTES À ESQUERDA) ---
Expanded( Expanded(
flex: 9,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Expanded( // 1. Lado Esquerdo: Donut Chart LIMPO (Sem texto sobreposto)
flex: 1, SizedBox(
width: cw * 0.38,
height: cw * 0.38,
child: PieChartWidget( child: PieChartWidget(
victoryPercentage: data.victoryPercentage, victoryPercentage: data.victoryPercentage,
defeatPercentage: data.defeatPercentage, defeatPercentage: data.defeatPercentage,
drawPercentage: data.drawPercentage, drawPercentage: data.drawPercentage,
sf: widget.sf, sf: widget.sf,
), ),
), ),
SizedBox(width: cw * 0.05),
SizedBox(width: cw * 0.08),
// 2. Lado Direito: Números Dinâmicos
Expanded( Expanded(
flex: 1, child: FittedBox(
child: Column( alignment: Alignment.centerLeft,
mainAxisAlignment: MainAxisAlignment.start, fit: BoxFit.scaleDown,
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
_buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, ch), crossAxisAlignment: CrossAxisAlignment.start,
_buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.yellow, ch), children: [
_buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, ch), _buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.greenAccent, ch, cw),
_buildDynDivider(ch), _buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.yellowAccent, ch, cw),
_buildDynStatRow("TOT", data.total.toString(), "100", Colors.white, ch), _buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.redAccent, ch, cw),
], _buildDynDivider(cw),
_buildDynStatRow("TOT", data.total.toString(), "100", Colors.white, ch, cw),
],
),
), ),
), ),
], ],
), ),
), ),
// 👇 RODAPÉ AJUSTADO const Expanded(flex: 1, child: SizedBox()),
SizedBox(height: ch * 0.03),
// --- RODAPÉ: BOTÃO WIN RATE GIGANTE ---
Container( Container(
width: double.infinity, width: double.infinity,
padding: EdgeInsets.symmetric(vertical: ch * 0.035), padding: EdgeInsets.symmetric(vertical: ch * 0.025),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white24, // Igual ao fundo do botão detalhes color: Colors.white.withOpacity(0.08),
borderRadius: BorderRadius.circular(ch * 0.03), // Borda arredondada borderRadius: BorderRadius.circular(12),
), ),
child: Center( child: FittedBox(
child: FittedBox( fit: BoxFit.scaleDown,
fit: BoxFit.scaleDown, child: Row(
child: Row( mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: [
children: [ Icon(Icons.stars, color: Colors.greenAccent, size: ch * 0.075),
Icon( const SizedBox(width: 10),
data.victoryPercentage >= 0.5 ? Icons.trending_up : Icons.trending_down, Text('WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
color: Colors.green, style: TextStyle(
size: ch * 0.09 color: Colors.white,
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
fontSize: ch * 0.06
), ),
SizedBox(width: cw * 0.02), ),
Text( ],
'WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: ch * 0.05,
fontWeight: FontWeight.bold,
color: Colors.white
)
),
],
),
), ),
), ),
), ),
@@ -183,34 +182,38 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
), ),
); );
} }
// 👇 PERCENTAGENS SUBIDAS LIGEIRAMENTE (0.10 e 0.045)
Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch) { Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch, double cw) {
return Padding( return Padding(
padding: EdgeInsets.only(bottom: ch * 0.01), padding: EdgeInsets.symmetric(vertical: ch * 0.005),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Número subiu para 0.10 SizedBox(
Expanded(flex: 2, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(number, style: TextStyle(fontSize: ch * 0.10, fontWeight: FontWeight.bold, color: color, height: 1.0)))), width: cw * 0.12,
SizedBox(width: ch * 0.02), child: Column(
Expanded( crossAxisAlignment: CrossAxisAlignment.end,
flex: 3, mainAxisSize: MainAxisSize.min,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ children: [
Row(children: [ Text(label, style: TextStyle(fontSize: ch * 0.035, color: Colors.white60, fontWeight: FontWeight.bold)),
Container(width: ch * 0.018, height: ch * 0.018, margin: EdgeInsets.only(right: ch * 0.015), decoration: BoxDecoration(color: color, shape: BoxShape.circle)), Text('$percent%', style: TextStyle(fontSize: ch * 0.04, color: color, fontWeight: FontWeight.bold)),
// Label subiu para 0.045 ],
Expanded(child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(label, style: TextStyle(fontSize: ch * 0.033, color: Colors.white.withOpacity(0.8), fontWeight: FontWeight.w600)))) ),
]),
// Percentagem subiu para 0.05
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('$percent%', style: TextStyle(fontSize: ch * 0.04, color: color, fontWeight: FontWeight.bold))),
]),
), ),
SizedBox(width: cw * 0.03),
Text(number, style: TextStyle(fontSize: ch * 0.125, fontWeight: FontWeight.w900, color: color, height: 1)),
], ],
), ),
); );
} }
Widget _buildDynDivider(double ch) { Widget _buildDynDivider(double cw) {
return Container(height: 0.5, color: Colors.white.withOpacity(0.1), margin: EdgeInsets.symmetric(vertical: ch * 0.01)); return Container(
width: cw * 0.35,
height: 1.5,
color: Colors.white24,
margin: const EdgeInsets.symmetric(vertical: 4)
);
} }
} }

View File

@@ -1,325 +1,367 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:playmaker/controllers/placar_controller.dart'; import 'package:playmaker/controllers/placar_controller.dart';
import 'package:playmaker/utils/size_extension.dart'; import 'package:playmaker/pages/heatmap_page.dart';
import 'package:playmaker/widgets/placar_widgets.dart'; import 'package:playmaker/utils/size_extension.dart';
import 'dart:math' as math; import 'package:playmaker/widgets/placar_widgets.dart';
import 'dart:math' as math;
class PlacarPage extends StatefulWidget { class PlacarPage extends StatefulWidget {
final String gameId, myTeam, opponentTeam; final String gameId, myTeam, opponentTeam;
const PlacarPage({super.key, required this.gameId, required this.myTeam, required this.opponentTeam}); const PlacarPage({super.key, required this.gameId, required this.myTeam, required this.opponentTeam});
@override @override
State<PlacarPage> createState() => _PlacarPageState(); State<PlacarPage> createState() => _PlacarPageState();
}
class _PlacarPageState extends State<PlacarPage> {
late PlacarController _controller;
@override
void initState() {
super.initState();
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeLeft,
]);
_controller = PlacarController(
gameId: widget.gameId,
myTeam: widget.myTeam,
opponentTeam: widget.opponentTeam,
onUpdate: () {
if (mounted) setState(() {});
}
);
_controller.loadPlayers();
} }
class _PlacarPageState extends State<PlacarPage> { @override
late PlacarController _controller; void dispose() {
_controller.dispose();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
super.dispose();
}
@override Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top, double sf) {
void initState() { return Positioned(
super.initState(); top: top, left: left > 0 ? left : null, right: right > 0 ? right : null,
SystemChrome.setPreferredOrientations([ child: Draggable<String>(
DeviceOrientation.landscapeRight, data: action,
DeviceOrientation.landscapeLeft, feedback: Material(color: Colors.transparent, child: CircleAvatar(radius: 30 * sf, backgroundColor: color.withOpacity(0.8), child: Icon(icon, color: Colors.white, size: 30 * sf))),
]); child: Column(
children: [
_controller = PlacarController( CircleAvatar(radius: 27 * sf, backgroundColor: color, child: Icon(icon, color: Colors.white, size: 28 * sf)),
gameId: widget.gameId, SizedBox(height: 5 * sf),
myTeam: widget.myTeam, Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 * sf)),
opponentTeam: widget.opponentTeam, ],
onUpdate: () { ),
if (mounted) setState(() {}); ),
} );
); }
_controller.loadPlayers();
Widget _buildCornerBtn({required String heroTag, required IconData icon, required Color color, required VoidCallback onTap, required double size, bool isLoading = false}) {
return SizedBox(
width: size, height: size,
child: FloatingActionButton(
heroTag: heroTag, backgroundColor: color,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * (size / 50))),
elevation: 5, onPressed: isLoading ? null : onTap,
child: isLoading ? SizedBox(width: size*0.45, height: size*0.45, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5)) : Icon(icon, color: Colors.white, size: size * 0.55),
),
);
}
@override
Widget build(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
// Calcula o tamanho normal
double sf = math.min(wScreen / 1150, hScreen / 720);
// 👇 O TRAVÃO DE MÃO PARA OS TABLETS 👇
sf = math.min(sf, 0.9);
final double cornerBtnSize = 48 * sf;
if (_controller.isLoading) {
return Scaffold(backgroundColor: const Color(0xFF16202C), body: Center(child: Text("PREPARANDO O PAVILHÃO...", style: TextStyle(color: Colors.white24, fontSize: 45 * sf, fontWeight: FontWeight.bold))));
} }
@override return Scaffold(
void dispose() { backgroundColor: const Color(0xFF266174),
_controller.dispose(); body: SafeArea(
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); top: false, bottom: false,
super.dispose(); child: IgnorePointer(
} ignoring: _controller.isSaving,
child: Stack(
// --- BOTÕES FLUTUANTES DE FALTA ---
Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top, double sf) {
return Positioned(
top: top,
left: left > 0 ? left : null,
right: right > 0 ? right : null,
child: Draggable<String>(
data: action,
feedback: Material(
color: Colors.transparent,
child: CircleAvatar(
radius: 30 * sf,
backgroundColor: color.withOpacity(0.8),
child: Icon(icon, color: Colors.white, size: 30 * sf)
),
),
child: Column(
children: [ children: [
CircleAvatar( // ==========================================
radius: 27 * sf, // --- 1. O CAMPO ---
backgroundColor: color, // ==========================================
child: Icon(icon, color: Colors.white, size: 28 * sf), Container(
), margin: EdgeInsets.only(left: 65 * sf, right: 65 * sf, bottom: 55 * sf),
SizedBox(height: 5 * sf), decoration: BoxDecoration(
Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 * sf)), border: Border.all(color: Colors.white, width: 2.5),
], image: const DecorationImage(image: AssetImage('assets/campo.png'), fit: BoxFit.fill),
),
),
);
}
// --- BOTÕES LATERAIS QUADRADOS ---
Widget _buildCornerBtn({required String heroTag, required IconData icon, required Color color, required VoidCallback onTap, required double size, bool isLoading = false}) {
return SizedBox(
width: size,
height: size,
child: FloatingActionButton(
heroTag: heroTag,
backgroundColor: color,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * (size / 50))),
elevation: 5,
onPressed: isLoading ? null : onTap,
child: isLoading
? SizedBox(width: size*0.45, height: size*0.45, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5))
: Icon(icon, color: Colors.white, size: size * 0.55),
),
);
}
@override
Widget build(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
// 👇 CÁLCULO MANUAL DO SF 👇
final double sf = math.min(wScreen / 1150, hScreen / 720);
final double cornerBtnSize = 48 * sf; // Tamanho ideal (Nem 38 nem 55)
if (_controller.isLoading) {
return Scaffold(
backgroundColor: const Color(0xFF16202C),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("PREPARANDO O PAVILHÃO", style: TextStyle(color: Colors.white24, fontSize: 45 * sf, fontWeight: FontWeight.bold, letterSpacing: 2)),
SizedBox(height: 35 * sf),
StreamBuilder(
stream: Stream.periodic(const Duration(seconds: 3)),
builder: (context, snapshot) {
List<String> frases = [
"O Treinador está a desenhar a tática...",
"A encher as bolas com ar de campeão...",
"O árbitro está a testar o apito...",
"A verificar se o cesto está nivelado...",
"Os jogadores estão a terminar o aquecimento..."
];
String frase = frases[DateTime.now().second % frases.length];
return Text(frase, style: TextStyle(color: Colors.orange.withOpacity(0.7), fontSize: 26 * sf, fontStyle: FontStyle.italic));
},
), ),
], child: LayoutBuilder(
), builder: (context, constraints) {
), final w = constraints.maxWidth;
); final h = constraints.maxHeight;
}
return Scaffold( return Stack(
backgroundColor: const Color(0xFF266174), children: [
body: SafeArea( Positioned.fill(
top: false, child: GestureDetector(
bottom: false, behavior: HitTestBehavior.opaque,
// 👇 A MÁGICA DO IGNORE POINTER COMEÇA AQUI 👇
child: IgnorePointer(
ignoring: _controller.isSaving, // Se estiver a gravar, ignora os toques!
child: Stack(
children: [
// --- O CAMPO ---
Container(
margin: EdgeInsets.only(left: 65 * sf, right: 65 * sf, bottom: 55 * sf),
decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.5)),
child: LayoutBuilder(
builder: (context, constraints) {
final w = constraints.maxWidth;
final h = constraints.maxHeight;
return Stack(
children: [
GestureDetector(
onTapDown: (details) { onTapDown: (details) {
if (_controller.isSelectingShotLocation) { if (_controller.isSelectingShotLocation) {
_controller.registerShotLocation(context, details.localPosition, Size(w, h)); _controller.registerShotLocation(context, details.localPosition, Size(w, h));
} }
}, },
child: Container( child: Stack(
decoration: const BoxDecoration( children: _controller.matchShots.map((shot) => Positioned(
image: DecorationImage( left: (shot.relativeX * w) - (9 * sf),
image: AssetImage('assets/campo.png'), top: (shot.relativeY * h) - (9 * sf),
fit: BoxFit.fill, child: CircleAvatar(radius: 9 * sf, backgroundColor: shot.isMake ? Colors.green : Colors.red, child: Icon(shot.isMake ? Icons.check : Icons.close, size: 11 * sf, color: Colors.white)),
), )).toList(),
),
child: Stack(
children: _controller.matchShots.map((shot) => Positioned(
// Agora usamos relativeX e relativeY multiplicados pela largura(w) e altura(h)
left: (shot.relativeX * w) - (9 * context.sf),
top: (shot.relativeY * h) - (9 * context.sf),
child: CircleAvatar(
radius: 9 * context.sf,
backgroundColor: shot.isMake ? Colors.green : Colors.red,
child: Icon(shot.isMake ? Icons.check : Icons.close, size: 11 * context.sf, color: Colors.white)
),
)).toList(),
),
), ),
), ),
),
// --- JOGADORES --- // --- JOGADORES ---
if (!_controller.isSelectingShotLocation) ...[ if (!_controller.isSelectingShotLocation) ...[
Positioned(top: h * 0.25, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[0], isOpponent: false, sf: sf)), Positioned(top: h * 0.25, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[0], isOpponent: false, sf: sf)),
Positioned(top: h * 0.68, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[1], isOpponent: false, sf: sf)), Positioned(top: h * 0.68, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[1], isOpponent: false, sf: sf)),
Positioned(top: h * 0.45, left: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[2], isOpponent: false, sf: sf)), Positioned(top: h * 0.45, left: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[2], isOpponent: false, sf: sf)),
Positioned(top: h * 0.15, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[3], isOpponent: false, sf: sf)), Positioned(top: h * 0.15, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[3], isOpponent: false, sf: sf)),
Positioned(top: h * 0.80, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[4], isOpponent: false, sf: sf)), Positioned(top: h * 0.80, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[4], isOpponent: false, sf: sf)),
Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[0], isOpponent: true, sf: sf)),
Positioned(top: h * 0.68, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[1], isOpponent: true, sf: sf)),
Positioned(top: h * 0.45, right: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[2], isOpponent: true, sf: sf)),
Positioned(top: h * 0.15, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[3], isOpponent: true, sf: sf)),
Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[4], isOpponent: true, sf: sf)),
],
// --- BOTÕES DE FALTAS ---
if (!_controller.isSelectingShotLocation) ...[
_buildFloatingFoulBtn("FALTA +", Colors.orange, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31, sf),
_buildFloatingFoulBtn("FALTA -", Colors.redAccent, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31, sf),
],
// --- BOTÃO PLAY/PAUSE ---
if (!_controller.isSelectingShotLocation)
Positioned(
top: (h * 0.32) + (40 * sf),
left: 0, right: 0,
child: Center(
child: GestureDetector(
onTap: () => _controller.toggleTimer(context),
child: CircleAvatar(
radius: 68 * sf,
backgroundColor: Colors.grey.withOpacity(0.5),
child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 58 * sf)
),
),
),
),
// --- PLACAR NO TOPO ---
Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))),
// --- BOTÕES DE AÇÃO --- Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[0], isOpponent: true, sf: sf)),
if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller, sf: sf)), Positioned(top: h * 0.68, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[1], isOpponent: true, sf: sf)),
Positioned(top: h * 0.45, right: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[2], isOpponent: true, sf: sf)),
// --- OVERLAY LANÇAMENTO --- Positioned(top: h * 0.15, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[3], isOpponent: true, sf: sf)),
if (_controller.isSelectingShotLocation) Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[4], isOpponent: true, sf: sf)),
Positioned(
top: h * 0.4, left: 0, right: 0,
child: Center(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 35 * sf, vertical: 18 * sf),
decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(11 * sf), border: Border.all(color: Colors.white, width: 1.5 * sf)),
child: Text("TOQUE NO CAMPO PARA MARCAR O LOCAL DO LANÇAMENTO", style: TextStyle(color: Colors.white, fontSize: 27 * sf, fontWeight: FontWeight.bold)),
),
),
),
], ],
);
}, // --- BOTÕES DE FALTAS ---
), if (!_controller.isSelectingShotLocation) ...[
_buildFloatingFoulBtn("FALTA +", Colors.orange, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31, sf),
_buildFloatingFoulBtn("FALTA -", Colors.redAccent, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31, sf),
],
// --- BOTÃO PLAY/PAUSE ---
if (!_controller.isSelectingShotLocation)
Positioned(
top: (h * 0.36) + (40 * sf),
left: 0, right: 0,
child: Center(
child: GestureDetector(
onTap: () => _controller.toggleTimer(context),
child: CircleAvatar(
radius: 68 * sf,
backgroundColor: Colors.grey.withOpacity(0.5),
child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 58 * sf)
)
)
)
),
Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))),
],
);
},
),
),
// ==========================================
// --- 2. O RODAPÉ (BOTÕES DE JOGO) ---
// ==========================================
if (!_controller.isSelectingShotLocation)
Positioned(
bottom: 60 * sf,
left: 0,
right: 0,
child: ActionButtonsPanel(controller: _controller, sf: sf)
), ),
// --- BOTÕES LATERAIS --- // ==========================================
// Topo Esquerdo: Guardar e Sair (Botão Único) // --- 3. BOTÕES LATERAIS ---
Positioned( // ==========================================
top: 50 * sf, left: 12 * sf,
child: _buildCornerBtn( Positioned(top: 50 * sf, left: 12 * sf, child: _buildCornerBtn(heroTag: 'btn_save_exit', icon: Icons.save_alt, color: const Color(0xFFD92C2C), size: cornerBtnSize, isLoading: _controller.isSaving, onTap: () async { await _controller.saveGameStats(context); if (context.mounted) Navigator.pop(context); })),
heroTag: 'btn_save_exit',
icon: Icons.save_alt,
color: const Color(0xFFD92C2C),
size: cornerBtnSize,
isLoading: _controller.isSaving,
onTap: () async {
// 1. Primeiro obriga a guardar os dados na BD
await _controller.saveGameStats(context);
// 2. Só depois de acabar de guardar é que volta para trás
if (context.mounted) {
Navigator.pop(context);
}
}
),
),
// Base Esquerda: Banco Casa + TIMEOUT DA CASA Positioned(top: 50 * sf, right: 12 * sf, child: _buildCornerBtn(heroTag: 'btn_heatmap', icon: Icons.analytics_outlined, color: Colors.purple.shade700, size: cornerBtnSize, onTap: () { if (_controller.matchShots.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Ainda não há lançamentos!'))); return; } Navigator.push(context, MaterialPageRoute(builder: (context) => HeatmapPage(shots: _controller.matchShots, teamName: _controller.myTeam))); })),
Positioned(
bottom: 55 * sf, left: 12 * sf, Positioned(bottom: 55 * sf, left: 12 * sf, child: Column(mainAxisSize: MainAxisSize.min, children: [ if (_controller.showMyBench) BenchPlayersList(controller: _controller, isOpponent: false, sf: sf), SizedBox(height: 12 * sf), _buildCornerBtn(heroTag: 'btn_sub_home', icon: Icons.swap_horiz, color: const Color(0xFF1E5BB2), size: cornerBtnSize, onTap: () { _controller.showMyBench = !_controller.showMyBench; _controller.onUpdate(); }), SizedBox(height: 12 * sf), _buildCornerBtn(heroTag: 'btn_to_home', icon: Icons.timer, color: _controller.myTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFF1E5BB2), size: cornerBtnSize, onTap: _controller.myTimeoutsUsed >= 3 ? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 Esgotado!'), backgroundColor: Colors.red)) : () => _controller.useTimeout(false))])),
Positioned(bottom: 55 * sf, right: 12 * sf, child: Column(mainAxisSize: MainAxisSize.min, children: [ if (_controller.showOppBench) BenchPlayersList(controller: _controller, isOpponent: true, sf: sf), SizedBox(height: 12 * sf), _buildCornerBtn(heroTag: 'btn_sub_away', icon: Icons.swap_horiz, color: const Color(0xFFD92C2C), size: cornerBtnSize, onTap: () { _controller.showOppBench = !_controller.showOppBench; _controller.onUpdate(); }), SizedBox(height: 12 * sf), _buildCornerBtn(heroTag: 'btn_to_away', icon: Icons.timer, color: _controller.opponentTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFFD92C2C), size: cornerBtnSize, onTap: _controller.opponentTimeoutsUsed >= 3 ? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 Esgotado!'), backgroundColor: Colors.red)) : () => _controller.useTimeout(true))])),
if (_controller.isSaving) Positioned.fill(child: Container(color: Colors.black.withOpacity(0.4))),
],
),
),
),
);
}
}
// ==============================================================
// 🏀 WIDGETS AUXILIARES (TopScoreboard, ActionButtonsPanel, etc)
// ==============================================================
class TopScoreboard extends StatelessWidget {
final PlacarController controller;
final double sf;
const TopScoreboard({super.key, required this.controller, required this.sf});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 10 * sf, horizontal: 35 * sf),
decoration: BoxDecoration(color: const Color(0xFF16202C), borderRadius: BorderRadius.only(bottomLeft: Radius.circular(22 * sf), bottomRight: Radius.circular(22 * sf)), border: Border.all(color: Colors.white, width: 2.5 * sf)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false, sf),
SizedBox(width: 30 * sf),
Column(mainAxisSize: MainAxisSize.min, children: [Container(padding: EdgeInsets.symmetric(horizontal: 18 * sf, vertical: 5 * sf), decoration: BoxDecoration(color: const Color(0xFF2C3E50), borderRadius: BorderRadius.circular(9 * sf)), child: Text(controller.formatTime(), style: TextStyle(color: Colors.white, fontSize: 28 * sf, fontWeight: FontWeight.w900, fontFamily: 'monospace', letterSpacing: 2 * sf))), SizedBox(height: 5 * sf), Text("PERÍODO ${controller.currentQuarter}", style: TextStyle(color: Colors.orangeAccent, fontSize: 14 * sf, fontWeight: FontWeight.w900))]),
SizedBox(width: 30 * sf),
_buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true, sf),
],
),
);
}
Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp, double sf) {
int displayFouls = fouls > 5 ? 5 : fouls;
final timeoutIndicators = Row(mainAxisSize: MainAxisSize.min, children: List.generate(3, (index) => Container(margin: EdgeInsets.symmetric(horizontal: 3.5 * sf), width: 12 * sf, height: 12 * sf, decoration: BoxDecoration(shape: BoxShape.circle, color: index < timeouts ? Colors.yellow : Colors.grey.shade600, border: Border.all(color: Colors.white54, width: 1.5 * sf)))));
List<Widget> content = [Column(children: [_scoreBox(score, color, sf), SizedBox(height: 7 * sf), timeoutIndicators]), SizedBox(width: 18 * sf), Column(crossAxisAlignment: isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end, children: [Text(name.toUpperCase(), style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.2 * sf)), SizedBox(height: 5 * sf), Text("FALTAS: $displayFouls", style: TextStyle(color: displayFouls >= 5 ? Colors.redAccent : Colors.yellowAccent, fontSize: 13 * sf, fontWeight: FontWeight.bold))])];
return Row(crossAxisAlignment: CrossAxisAlignment.center, children: isOpp ? content : content.reversed.toList());
}
Widget _scoreBox(int score, Color color, double sf) => Container(width: 58 * sf, height: 45 * sf, alignment: Alignment.center, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(7 * sf)), child: Text(score.toString(), style: TextStyle(color: Colors.white, fontSize: 26 * sf, fontWeight: FontWeight.w900)));
}
class BenchPlayersList extends StatelessWidget {
final PlacarController controller;
final bool isOpponent;
final double sf;
const BenchPlayersList({super.key, required this.controller, required this.isOpponent, required this.sf});
@override
Widget build(BuildContext context) {
final bench = isOpponent ? controller.oppBench : controller.myBench;
final teamColor = isOpponent ? const Color(0xFFD92C2C) : const Color(0xFF1E5BB2);
final prefix = isOpponent ? "bench_opp_" : "bench_my_";
return Column(mainAxisSize: MainAxisSize.min, children: bench.map((playerName) {
final num = controller.playerNumbers[playerName] ?? "0";
final bool isFouledOut = (controller.playerStats[playerName]?["fls"] ?? 0) >= 5;
Widget avatarUI = Container(margin: EdgeInsets.only(bottom: 7 * sf), decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 1.8 * sf), boxShadow: [BoxShadow(color: Colors.black45, blurRadius: 5 * sf, offset: Offset(0, 2.5 * sf))]), child: CircleAvatar(radius: 22 * sf, backgroundColor: isFouledOut ? Colors.grey.shade800 : teamColor, child: Text(num, style: TextStyle(color: isFouledOut ? Colors.red.shade300 : Colors.white, fontSize: 16 * sf, fontWeight: FontWeight.bold, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none))));
if (isFouledOut) return GestureDetector(onTap: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $playerName não pode voltar (Expulso).'), backgroundColor: Colors.red)), child: avatarUI);
return Draggable<String>(data: "$prefix$playerName", feedback: Material(color: Colors.transparent, child: CircleAvatar(radius: 28 * sf, backgroundColor: teamColor, child: Text(num, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18 * sf)))), childWhenDragging: Opacity(opacity: 0.5, child: SizedBox(width: 45 * sf, height: 45 * sf)), child: avatarUI);
}).toList());
}
}
class PlayerCourtCard extends StatelessWidget {
final PlacarController controller;
final String name;
final bool isOpponent;
final double sf;
const PlayerCourtCard({super.key, required this.controller, required this.name, required this.isOpponent, required this.sf});
@override
Widget build(BuildContext context) {
final teamColor = isOpponent ? const Color(0xFFD92C2C) : const Color(0xFF1E5BB2);
final stats = controller.playerStats[name]!;
final number = controller.playerNumbers[name]!;
final prefix = isOpponent ? "player_opp_" : "player_my_";
final int fouls = stats["fls"] ?? 0;
return Draggable<String>(
data: "$prefix$name",
feedback: Material(color: Colors.transparent, child: Container(padding: EdgeInsets.symmetric(horizontal: 18 * sf, vertical: 11 * sf), decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(9 * sf)), child: Text(name, style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)))),
childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(context, number, name, stats, teamColor, false, false, sf, fouls)),
child: DragTarget<String>(
onAcceptWithDetails: (details) {
final action = details.data;
if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) controller.handleActionDrag(context, action, "$prefix$name");
else if (action.startsWith("bench_")) controller.handleSubbing(context, action, name, isOpponent);
},
builder: (context, candidateData, rejectedData) => _playerCardUI(
context,
number,
name,
stats,
teamColor,
candidateData.any((d) => d != null && d.startsWith("bench_")),
candidateData.any((d) => d != null && (d.startsWith("add_") || d.startsWith("sub_") || d.startsWith("miss_"))),
sf,
fouls),
),
);
}
Widget _playerCardUI(BuildContext context, String number, String name, Map<String, int> stats, Color teamColor, bool isSubbing, bool isActionHover, double sf, int fouls) {
bool isFouledOut = fouls >= 5;
Color bgColor = isFouledOut ? Colors.red.shade50 : (isSubbing ? Colors.blue.shade50 : (isActionHover ? Colors.orange.shade50 : Colors.white));
Color borderColor = isFouledOut ? Colors.redAccent : (isSubbing ? Colors.blue : (isActionHover ? Colors.orange : Colors.transparent));
int fgm = stats["fgm"]!; int fga = stats["fga"]!;
String fgPercent = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) : "0";
String displayName = name.length > 12 ? "${name.substring(0, 10)}..." : name;
return GestureDetector(
onTap: () {
final playerShots = controller.matchShots.where((s) => s.playerName == name).toList();
if (playerShots.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('O $name ainda não lançou!')));
return;
}
Navigator.push(context, MaterialPageRoute(builder: (context) => HeatmapPage(shots: playerShots, teamName: name)));
},
child: Container(
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(11 * sf),
border: Border.all(color: borderColor, width: 2 * sf),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5 * sf, offset: Offset(2 * sf, 3.5 * sf))]
),
child: ClipRRect(
borderRadius: BorderRadius.circular(9 * sf),
child: IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// --- LADO ESQUERDO: APENAS O NÚMERO ---
Container(
padding: EdgeInsets.symmetric(horizontal: 16 * sf),
color: isFouledOut ? Colors.grey[700] : teamColor,
alignment: Alignment.center,
child: Text(number, style: TextStyle(color: Colors.white, fontSize: 24 * sf, fontWeight: FontWeight.bold)),
),
// --- LADO DIREITO: INFO ---
Padding(
padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 7 * sf),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (_controller.showMyBench) BenchPlayersList(controller: _controller, isOpponent: false, sf: sf), Text(displayName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? Colors.red : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)),
SizedBox(height: 12 * sf), SizedBox(height: 2 * sf),
_buildCornerBtn(heroTag: 'btn_sub_home', icon: Icons.swap_horiz, color: const Color(0xFF1E5BB2), size: cornerBtnSize, onTap: () { _controller.showMyBench = !_controller.showMyBench; _controller.onUpdate(); }), Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[700], fontWeight: FontWeight.bold)),
SizedBox(height: 12 * sf), // Texto de faltas com destaque se estiver em perigo (4 ou 5)
_buildCornerBtn( Text("AST: ${stats["ast"]} | REB: ${stats["orb"]! + stats["drb"]!} | FALTAS: $fouls",
heroTag: 'btn_to_home', style: TextStyle(
icon: Icons.timer, fontSize: 11 * sf,
color: _controller.myTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFF1E5BB2), color: fouls >= 4 ? Colors.red : Colors.grey[600],
size: cornerBtnSize, fontWeight: fouls >= 4 ? FontWeight.w900 : FontWeight.w600
onTap: _controller.myTimeoutsUsed >= 3 )),
? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 A equipa da casa já usou os 3 Timeouts deste período!'), backgroundColor: Colors.red))
: () => _controller.useTimeout(false)
),
], ],
), ),
), )
// Base Direita: Banco Visitante + TIMEOUT DO VISITANTE
Positioned(
bottom: 55 * sf, right: 12 * sf,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_controller.showOppBench) BenchPlayersList(controller: _controller, isOpponent: true, sf: sf),
SizedBox(height: 12 * sf),
_buildCornerBtn(heroTag: 'btn_sub_away', icon: Icons.swap_horiz, color: const Color(0xFFD92C2C), size: cornerBtnSize, onTap: () { _controller.showOppBench = !_controller.showOppBench; _controller.onUpdate(); }),
SizedBox(height: 12 * sf),
_buildCornerBtn(
heroTag: 'btn_to_away',
icon: Icons.timer,
color: _controller.opponentTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFFD92C2C),
size: cornerBtnSize,
onTap: _controller.opponentTimeoutsUsed >= 3
? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 A equipa visitante já usou os 3 Timeouts deste período!'), backgroundColor: Colors.red))
: () => _controller.useTimeout(true)
),
],
),
),
// 👇 EFEITO VISUAL (Ecrã escurece para mostrar que está a carregar) 👇
if (_controller.isSaving)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.4),
),
),
], ],
), ),
), ),
), ),
); ),
} );
} }
}

View File

@@ -1,9 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/pages/PlacarPage.dart'; import 'package:playmaker/pages/PlacarPage.dart';
import '../controllers/game_controller.dart'; import '../controllers/game_controller.dart';
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../models/game_model.dart'; import '../models/game_model.dart';
import '../utils/size_extension.dart'; // 👇 NOVO SUPERPODER AQUI TAMBÉM! import '../utils/size_extension.dart';
import 'dart:math' as math; // 👇 IMPORTANTE PARA O TRAVÃO DE MÃO
// --- CARD DE EXIBIÇÃO DO JOGO --- // --- CARD DE EXIBIÇÃO DO JOGO ---
class GameResultCard extends StatelessWidget { class GameResultCard extends StatelessWidget {
@@ -18,59 +19,61 @@ class GameResultCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // TRAVÃO DO TABLET
return Container( return Container(
margin: EdgeInsets.only(bottom: 16 * context.sf), margin: EdgeInsets.only(bottom: 16 * safeSf),
padding: EdgeInsets.all(16 * context.sf), padding: EdgeInsets.all(16 * safeSf),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * context.sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * context.sf)]), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * safeSf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * safeSf)]),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: _buildTeamInfo(context, myTeam, const Color(0xFFE74C3C), myTeamLogo)), Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, safeSf)),
_buildScoreCenter(context, gameId), _buildScoreCenter(context, gameId, safeSf),
Expanded(child: _buildTeamInfo(context, opponentTeam, Colors.black87, opponentTeamLogo)), Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, safeSf)),
], ],
), ),
); );
} }
Widget _buildTeamInfo(BuildContext context, String name, Color color, String? logoUrl) { Widget _buildTeamInfo(String name, Color color, String? logoUrl, double safeSf) {
return Column( return Column(
children: [ children: [
CircleAvatar(radius: 24 * context.sf, backgroundColor: color, backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * context.sf) : null), CircleAvatar(radius: 24 * safeSf, backgroundColor: color, backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * safeSf) : null),
SizedBox(height: 6 * context.sf), SizedBox(height: 6 * safeSf),
Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2), Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * safeSf), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2),
], ],
); );
} }
Widget _buildScoreCenter(BuildContext context, String id) { Widget _buildScoreCenter(BuildContext context, String id, double safeSf) {
return Column( return Column(
children: [ children: [
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_scoreBox(context, myScore, Colors.green), _scoreBox(myScore, Colors.green, safeSf),
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * context.sf)), Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * safeSf)),
_scoreBox(context, opponentScore, Colors.grey), _scoreBox(opponentScore, Colors.grey, safeSf),
], ],
), ),
SizedBox(height: 10 * context.sf), SizedBox(height: 10 * safeSf),
TextButton.icon( TextButton.icon(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))),
icon: Icon(Icons.play_circle_fill, size: 18 * context.sf, color: const Color(0xFFE74C3C)), icon: Icon(Icons.play_circle_fill, size: 18 * safeSf, color: const Color(0xFFE74C3C)),
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * context.sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)), label: Text("RETORNAR", style: TextStyle(fontSize: 11 * safeSf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
style: TextButton.styleFrom(backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), padding: EdgeInsets.symmetric(horizontal: 14 * context.sf, vertical: 8 * context.sf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), visualDensity: VisualDensity.compact), style: TextButton.styleFrom(backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), padding: EdgeInsets.symmetric(horizontal: 14 * safeSf, vertical: 8 * safeSf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * safeSf)), visualDensity: VisualDensity.compact),
), ),
SizedBox(height: 6 * context.sf), SizedBox(height: 6 * safeSf),
Text(status, style: TextStyle(fontSize: 12 * context.sf, color: Colors.blue, fontWeight: FontWeight.bold)), Text(status, style: TextStyle(fontSize: 12 * safeSf, color: Colors.blue, fontWeight: FontWeight.bold)),
], ],
); );
} }
Widget _scoreBox(BuildContext context, String pts, Color c) => Container( Widget _scoreBox(String pts, Color c, double safeSf) => Container(
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf, vertical: 6 * context.sf), padding: EdgeInsets.symmetric(horizontal: 12 * safeSf, vertical: 6 * safeSf),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * context.sf)), decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * safeSf)),
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)), child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * safeSf)),
); );
} }
@@ -104,25 +107,30 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // TRAVÃO DO TABLET
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * safeSf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)), title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * safeSf)),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Container(
mainAxisSize: MainAxisSize.min, constraints: BoxConstraints(maxWidth: 450 * safeSf), // LIMITA A LARGURA NO TABLET
children: [ child: Column(
TextField(controller: _seasonController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * context.sf))), mainAxisSize: MainAxisSize.min,
SizedBox(height: 15 * context.sf), children: [
_buildSearch(context, "Minha Equipa", _myTeamController), TextField(controller: _seasonController, style: TextStyle(fontSize: 14 * safeSf), decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * safeSf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * safeSf))),
Padding(padding: EdgeInsets.symmetric(vertical: 10 * context.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * context.sf))), SizedBox(height: 15 * safeSf),
_buildSearch(context, "Adversário", _opponentController), _buildSearch(label: "Minha Equipa", controller: _myTeamController, safeSf: safeSf),
], Padding(padding: EdgeInsets.symmetric(vertical: 10 * safeSf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * safeSf))),
_buildSearch(label: "Adversário", controller: _opponentController, safeSf: safeSf),
],
),
), ),
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * context.sf))), TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * safeSf))),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)), style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * safeSf)), padding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 10 * safeSf)),
onPressed: _isLoading ? null : () async { onPressed: _isLoading ? null : () async {
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) { if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
setState(() => _isLoading = true); setState(() => _isLoading = true);
@@ -134,13 +142,13 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
} }
} }
}, },
child: _isLoading ? SizedBox(width: 20 * context.sf, height: 20 * context.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), child: _isLoading ? SizedBox(width: 20 * safeSf, height: 20 * safeSf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * safeSf)),
), ),
], ],
); );
} }
Widget _buildSearch(BuildContext context, String label, TextEditingController controller) { Widget _buildSearch({required String label, required TextEditingController controller, required double safeSf}) {
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: widget.teamController.teamsStream, stream: widget.teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -156,9 +164,9 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
return Align( return Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Material( child: Material(
elevation: 4.0, borderRadius: BorderRadius.circular(8 * context.sf), elevation: 4.0, borderRadius: BorderRadius.circular(8 * safeSf),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 250 * context.sf, maxWidth: MediaQuery.of(context).size.width * 0.7), constraints: BoxConstraints(maxHeight: 250 * safeSf, maxWidth: 400 * safeSf), // Limita também o dropdown
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
@@ -166,8 +174,8 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
final String name = option['name'].toString(); final String name = option['name'].toString();
final String? imageUrl = option['image_url']; final String? imageUrl = option['image_url'];
return ListTile( return ListTile(
leading: CircleAvatar(radius: 20 * context.sf, backgroundColor: Colors.grey.shade200, backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * context.sf) : null), leading: CircleAvatar(radius: 20 * safeSf, backgroundColor: Colors.grey.shade200, backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * safeSf) : null),
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * safeSf)),
onTap: () { onSelected(option); }, onTap: () { onSelected(option); },
); );
}, },
@@ -180,8 +188,8 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) txtCtrl.text = controller.text; if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) txtCtrl.text = controller.text;
txtCtrl.addListener(() { controller.text = txtCtrl.text; }); txtCtrl.addListener(() { controller.text = txtCtrl.text; });
return TextField( return TextField(
controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * context.sf), controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * safeSf),
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * context.sf), prefixIcon: Icon(Icons.search, size: 20 * context.sf), border: const OutlineInputBorder()), decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * safeSf), prefixIcon: Icon(Icons.search, size: 20 * safeSf), border: const OutlineInputBorder()),
); );
}, },
); );
@@ -190,6 +198,8 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
} }
} }
// (O RESTO DA CLASSE GamePage CONTINUA IGUAL, o sf nativo já estava protegido lá dentro)
// --- PÁGINA PRINCIPAL DOS JOGOS --- // --- PÁGINA PRINCIPAL DOS JOGOS ---
class GamePage extends StatefulWidget { class GamePage extends StatefulWidget {
const GamePage({super.key}); const GamePage({super.key});

View File

@@ -8,6 +8,7 @@ import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:playmaker/pages/status_page.dart'; import 'package:playmaker/pages/status_page.dart';
import '../utils/size_extension.dart'; import '../utils/size_extension.dart';
import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart'; import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart';
import 'dart:math' as math; // 👇 IMPORTANTE
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@@ -30,10 +31,10 @@ class _HomeScreenState extends State<HomeScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Já não precisamos calcular o sf aqui! final double safeSf = math.min(context.sf, 1.15); // TRAVÃO
final List<Widget> pages = [ final List<Widget> pages = [
_buildHomeContent(context), // Passamos só o context _buildHomeContent(context, safeSf), // Passamos o safeSf
const GamePage(), const GamePage(),
const TeamsPage(), const TeamsPage(),
const StatusPage(), const StatusPage(),
@@ -42,11 +43,11 @@ class _HomeScreenState extends State<HomeScreen> {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: AppBar( appBar: AppBar(
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)), title: Text('PlayMaker', style: TextStyle(fontSize: 20 * safeSf)),
backgroundColor: HomeConfig.primaryColor, backgroundColor: HomeConfig.primaryColor,
foregroundColor: Colors.white, foregroundColor: Colors.white,
leading: IconButton( leading: IconButton(
icon: Icon(Icons.person, size: 24 * context.sf), icon: Icon(Icons.person, size: 24 * safeSf),
onPressed: () {}, onPressed: () {},
), ),
), ),
@@ -62,8 +63,7 @@ class _HomeScreenState extends State<HomeScreen> {
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
elevation: 1, elevation: 1,
// O math.min não é necessário se já tens o sf. Mas podes usar context.sf height: 70 * safeSf,
height: 70 * (context.sf < 1.2 ? context.sf : 1.2),
destinations: const [ destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'), 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.sports_soccer_outlined), selectedIcon: Icon(Icons.sports_soccer), label: 'Jogo'),
@@ -74,16 +74,16 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
void _showTeamSelector(BuildContext context) { void _showTeamSelector(BuildContext context, double safeSf) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * safeSf))),
builder: (context) { builder: (context) {
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: _teamController.teamsStream, stream: _teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); if (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: const Center(child: Text("Nenhuma equipa criada."))); if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * safeSf, child: const Center(child: Text("Nenhuma equipa criada.")));
final teams = snapshot.data!; final teams = snapshot.data!;
return ListView.builder( return ListView.builder(
@@ -92,7 +92,7 @@ class _HomeScreenState extends State<HomeScreen> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final team = teams[index]; final team = teams[index];
return ListTile( return ListTile(
title: Text(team['name']), title: Text(team['name'], style: TextStyle(fontSize: 16 * safeSf)),
onTap: () { onTap: () {
setState(() { setState(() {
_selectedTeamId = team['id']; _selectedTeamId = team['id'];
@@ -112,9 +112,10 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
Widget _buildHomeContent(BuildContext context) { Widget _buildHomeContent(BuildContext context, double safeSf) {
final double wScreen = MediaQuery.of(context).size.width; final double wScreen = MediaQuery.of(context).size.width;
final double cardHeight = wScreen * 0.5; // Evita que os cartões fiquem muito altos no tablet:
final double cardHeight = math.min(wScreen * 0.5, 200 * safeSf);
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: _selectedTeamId != null stream: _selectedTeamId != null
@@ -125,44 +126,44 @@ class _HomeScreenState extends State<HomeScreen> {
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: 22.0 * context.sf, vertical: 16.0 * context.sf), padding: EdgeInsets.symmetric(horizontal: 22.0 * safeSf, vertical: 16.0 * safeSf),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
InkWell( InkWell(
onTap: () => _showTeamSelector(context), onTap: () => _showTeamSelector(context, safeSf),
child: Container( child: Container(
padding: EdgeInsets.all(12 * context.sf), padding: EdgeInsets.all(12 * safeSf),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.shade300)), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * safeSf), border: Border.all(color: Colors.grey.shade300)),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row(children: [Icon(Icons.shield, color: HomeConfig.primaryColor, size: 24 * context.sf), SizedBox(width: 10 * context.sf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold))]), Row(children: [Icon(Icons.shield, color: HomeConfig.primaryColor, size: 24 * safeSf), SizedBox(width: 10 * safeSf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * safeSf, fontWeight: FontWeight.bold))]),
const Icon(Icons.arrow_drop_down), const Icon(Icons.arrow_drop_down),
], ],
), ),
), ),
), ),
SizedBox(height: 20 * context.sf), SizedBox(height: 20 * safeSf),
SizedBox( SizedBox(
height: cardHeight, height: cardHeight,
child: Row( child: Row(
children: [ children: [
Expanded(child: _buildStatCard(context: context, title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)), Expanded(child: _buildStatCard(context: context, title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)),
SizedBox(width: 12 * context.sf), SizedBox(width: 12 * safeSf),
Expanded(child: _buildStatCard(context: context, title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))), Expanded(child: _buildStatCard(context: context, title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))),
], ],
), ),
), ),
SizedBox(height: 12 * context.sf), SizedBox(height: 12 * safeSf),
SizedBox( SizedBox(
height: cardHeight, height: cardHeight,
child: Row( child: Row(
children: [ children: [
Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))), Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))),
SizedBox(width: 12 * context.sf), SizedBox(width: 12 * safeSf),
Expanded( Expanded(
child: PieChartCard( child: PieChartCard(
victories: _teamWins, victories: _teamWins,
@@ -171,22 +172,22 @@ class _HomeScreenState extends State<HomeScreen> {
title: 'DESEMPENHO', title: 'DESEMPENHO',
subtitle: 'Temporada', subtitle: 'Temporada',
backgroundColor: const Color(0xFFC62828), backgroundColor: const Color(0xFFC62828),
sf: context.sf // Aqui o PieChartCard ainda usa sf, então passamos sf: safeSf
), ),
), ),
], ],
), ),
), ),
SizedBox(height: 40 * context.sf), SizedBox(height: 40 * safeSf),
Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[800])), Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * safeSf, fontWeight: FontWeight.bold, color: Colors.grey[800])),
SizedBox(height: 16 * context.sf), SizedBox(height: 16 * safeSf),
_selectedTeamName == "Selecionar Equipa" _selectedTeamName == "Selecionar Equipa"
? Container( ? Container(
padding: EdgeInsets.all(20 * context.sf), padding: EdgeInsets.all(20 * safeSf),
alignment: Alignment.center, alignment: Alignment.center,
child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)), child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * safeSf)),
) )
: StreamBuilder<List<Map<String, dynamic>>>( : StreamBuilder<List<Map<String, dynamic>>>(
stream: _supabase.from('games').stream(primaryKey: ['id']) stream: _supabase.from('games').stream(primaryKey: ['id'])
@@ -206,7 +207,7 @@ class _HomeScreenState extends State<HomeScreen> {
if (gamesList.isEmpty) { if (gamesList.isEmpty) {
return Container( return Container(
padding: EdgeInsets.all(20 * context.sf), padding: EdgeInsets.all(20 * safeSf),
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)), decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)),
alignment: Alignment.center, alignment: Alignment.center,
child: Text("Ainda não há jogos terminados para $_selectedTeamName.", style: TextStyle(color: Colors.grey)), child: Text("Ainda não há jogos terminados para $_selectedTeamName.", style: TextStyle(color: Colors.grey)),
@@ -236,7 +237,7 @@ class _HomeScreenState extends State<HomeScreen> {
if (myScore < oppScore) result = 'D'; if (myScore < oppScore) result = 'D';
return _buildGameHistoryCard( return _buildGameHistoryCard(
context: context, // Usamos o context para o sf context: context,
opponent: opponent, opponent: opponent,
result: result, result: result,
myScore: myScore, myScore: myScore,
@@ -247,13 +248,14 @@ class _HomeScreenState extends State<HomeScreen> {
topRbs: game['top_rbs_name'] ?? '---', topRbs: game['top_rbs_name'] ?? '---',
topDef: game['top_def_name'] ?? '---', topDef: game['top_def_name'] ?? '---',
mvp: game['mvp_name'] ?? '---', mvp: game['mvp_name'] ?? '---',
safeSf: safeSf // Passa a escala aqui
); );
}).toList(), }).toList(),
); );
}, },
), ),
SizedBox(height: 20 * context.sf), SizedBox(height: 20 * safeSf),
], ],
), ),
), ),
@@ -323,14 +325,14 @@ class _HomeScreenState extends State<HomeScreen> {
Widget _buildGameHistoryCard({ Widget _buildGameHistoryCard({
required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date, 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 required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp, required double safeSf
}) { }) {
bool isWin = result == 'V'; bool isWin = result == 'V';
bool isDraw = result == 'E'; bool isDraw = result == 'E';
Color statusColor = isWin ? Colors.green : (isDraw ? Colors.yellow.shade700 : Colors.red); Color statusColor = isWin ? Colors.green : (isDraw ? Colors.yellow.shade700 : Colors.red);
return Container( return Container(
margin: EdgeInsets.only(bottom: 14 * context.sf), margin: EdgeInsets.only(bottom: 14 * safeSf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(16), color: Colors.white, borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))], border: Border.all(color: Colors.grey.shade200), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))],
@@ -338,34 +340,34 @@ class _HomeScreenState extends State<HomeScreen> {
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: EdgeInsets.all(14 * context.sf), padding: EdgeInsets.all(14 * safeSf),
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 36 * context.sf, height: 36 * context.sf, width: 36 * safeSf, height: 36 * safeSf,
decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle), 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))), child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * safeSf))),
), ),
SizedBox(width: 14 * context.sf), SizedBox(width: 14 * safeSf),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(date, style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.w600)), Text(date, style: TextStyle(fontSize: 11 * safeSf, color: Colors.grey, fontWeight: FontWeight.w600)),
SizedBox(height: 6 * context.sf), SizedBox(height: 6 * safeSf),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)), Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * safeSf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf), padding: EdgeInsets.symmetric(horizontal: 8 * safeSf),
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), padding: EdgeInsets.symmetric(horizontal: 8 * safeSf, vertical: 4 * safeSf),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8)),
child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)), child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * safeSf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)),
), ),
), ),
Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)), Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * safeSf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)),
], ],
), ),
], ],
@@ -376,27 +378,27 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
Divider(height: 1, color: Colors.grey.shade100, thickness: 1.5), Divider(height: 1, color: Colors.grey.shade100, thickness: 1.5),
Container( Container(
width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf), width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 12 * safeSf),
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))), decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))),
child: Column( child: Column(
children: [ children: [
Row( Row(
children: [ children: [
Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)), Expanded(child: _buildGridStatRow(Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, safeSf, isMvp: true)),
Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef)), Expanded(child: _buildGridStatRow(Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef, safeSf)),
], ],
), ),
SizedBox(height: 8 * context.sf), SizedBox(height: 8 * safeSf),
Row( Row(
children: [ children: [
Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)), Expanded(child: _buildGridStatRow(Icons.bolt, Colors.blue.shade700, "Pontos", topPts, safeSf)),
Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs)), Expanded(child: _buildGridStatRow(Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs, safeSf)),
], ],
), ),
SizedBox(height: 8 * context.sf), SizedBox(height: 8 * safeSf),
Row( Row(
children: [ children: [
Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)), Expanded(child: _buildGridStatRow(Icons.star, Colors.green.shade700, "Assists", topAst, safeSf)),
const Expanded(child: SizedBox()), const Expanded(child: SizedBox()),
], ],
), ),
@@ -408,17 +410,17 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, String label, String value, {bool isMvp = false}) { Widget _buildGridStatRow(IconData icon, Color color, String label, String value, double safeSf, {bool isMvp = false}) {
return Row( return Row(
children: [ children: [
Icon(icon, size: 14 * context.sf, color: color), Icon(icon, size: 14 * safeSf, color: color),
SizedBox(width: 4 * context.sf), SizedBox(width: 4 * safeSf),
Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), Text('$label: ', style: TextStyle(fontSize: 11 * safeSf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
Expanded( Expanded(
child: Text( child: Text(
value, value,
style: TextStyle( style: TextStyle(
fontSize: 11 * context.sf, fontSize: 11 * safeSf,
color: isMvp ? Colors.amber.shade900 : Colors.black87, color: isMvp ? Colors.amber.shade900 : Colors.black87,
fontWeight: FontWeight.bold fontWeight: FontWeight.bold
), ),

View File

@@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../utils/size_extension.dart'; // 👇 A MAGIA DO SF! import '../utils/size_extension.dart';
import 'dart:math' as math;
import '../controllers/placar_controller.dart'; // Para a classe ShotRecord
import '../pages/heatmap_page.dart'; // Para abrir a página do mapa
class StatusPage extends StatefulWidget { class StatusPage extends StatefulWidget {
const StatusPage({super.key}); const StatusPage({super.key});
@@ -19,19 +22,70 @@ class _StatusPageState extends State<StatusPage> {
String _sortColumn = 'pts'; String _sortColumn = 'pts';
bool _isAscending = false; bool _isAscending = false;
// 👇 NOVA FUNÇÃO: BUSCA OS LANÇAMENTOS DO JOGADOR NO SUPABASE E ABRE O MAPA
Future<void> _openPlayerHeatmap(String playerName) async {
if (_selectedTeamId == null) return;
// Mostra um loading rápido
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator(color: Color(0xFFE74C3C)))
);
try {
final response = await _supabase
.from('game_shots')
.select()
.eq('team_id', _selectedTeamId!)
.eq('player_name', playerName);
if (mounted) Navigator.pop(context); // Fecha o loading
if (response == null || (response as List).isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('O $playerName ainda não tem lançamentos registados!'))
);
}
return;
}
final List<ShotRecord> shots = (response as List).map((s) => ShotRecord(
relativeX: (s['relative_x'] as num).toDouble(),
relativeY: (s['relative_y'] as num).toDouble(),
isMake: s['is_make'] as bool,
playerName: s['player_name'],
)).toList();
if (mounted) {
Navigator.push(context, MaterialPageRoute(
builder: (_) => HeatmapPage(shots: shots, teamName: "Mapa de: $playerName")
));
}
} catch (e) {
if (mounted) Navigator.pop(context);
debugPrint("Erro ao carregar heatmap: $e");
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15);
final double screenWidth = MediaQuery.of(context).size.width;
return Column( return Column(
children: [ children: [
// --- SELETOR DE EQUIPA ---
Padding( Padding(
padding: EdgeInsets.all(16.0 * context.sf), padding: EdgeInsets.all(16.0 * safeSf),
child: InkWell( child: InkWell(
onTap: () => _showTeamSelector(context), onTap: () => _showTeamSelector(context, safeSf),
child: Container( child: Container(
padding: EdgeInsets.all(12 * context.sf), padding: EdgeInsets.all(12 * safeSf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(15 * context.sf), borderRadius: BorderRadius.circular(15 * safeSf),
border: Border.all(color: Colors.grey.shade300), border: Border.all(color: Colors.grey.shade300),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)] boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)]
), ),
@@ -39,9 +93,9 @@ class _StatusPageState extends State<StatusPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row(children: [ Row(children: [
Icon(Icons.shield, color: const Color(0xFFE74C3C), size: 24 * context.sf), Icon(Icons.shield, color: const Color(0xFFE74C3C), size: 24 * safeSf),
SizedBox(width: 10 * context.sf), SizedBox(width: 10 * safeSf),
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold)) Text(_selectedTeamName, style: TextStyle(fontSize: 16 * safeSf, fontWeight: FontWeight.bold))
]), ]),
const Icon(Icons.arrow_drop_down), const Icon(Icons.arrow_drop_down),
], ],
@@ -50,9 +104,10 @@ class _StatusPageState extends State<StatusPage> {
), ),
), ),
// --- TABELA DE ESTATÍSTICAS ---
Expanded( Expanded(
child: _selectedTeamId == null child: _selectedTeamId == null
? Center(child: Text("Seleciona uma equipa acima.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf))) ? Center(child: Text("Seleciona uma equipa acima.", style: TextStyle(color: Colors.grey, fontSize: 14 * safeSf)))
: StreamBuilder<List<Map<String, dynamic>>>( : StreamBuilder<List<Map<String, dynamic>>>(
stream: _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!), stream: _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
builder: (context, statsSnapshot) { builder: (context, statsSnapshot) {
@@ -67,7 +122,7 @@ class _StatusPageState extends State<StatusPage> {
} }
final membersData = membersSnapshot.data ?? []; final membersData = membersSnapshot.data ?? [];
if (membersData.isEmpty) return Center(child: Text("Esta equipa não tem jogadores registados.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf))); if (membersData.isEmpty) return Center(child: Text("Esta equipa não tem jogadores registados.", style: TextStyle(color: Colors.grey, fontSize: 14 * safeSf)));
final statsData = statsSnapshot.data ?? []; final statsData = statsSnapshot.data ?? [];
final gamesData = gamesSnapshot.data ?? []; final gamesData = gamesSnapshot.data ?? [];
@@ -82,7 +137,7 @@ class _StatusPageState extends State<StatusPage> {
return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA); return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA);
}); });
return _buildStatsGrid(context, playerTotals, teamTotals); return _buildStatsGrid(context, playerTotals, teamTotals, safeSf, screenWidth);
} }
); );
} }
@@ -94,29 +149,21 @@ class _StatusPageState extends State<StatusPage> {
); );
} }
// (Lógica de _aggregateStats e _calculateTeamTotals continua igual...)
List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) { List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
Map<String, Map<String, dynamic>> aggregated = {}; Map<String, Map<String, dynamic>> aggregated = {};
for (var member in members) { for (var member in members) {
String name = member['name']?.toString() ?? "Desconhecido"; String name = member['name']?.toString() ?? "Desconhecido";
aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0}; aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
} }
for (var row in stats) { for (var row in stats) {
String name = row['player_name']?.toString() ?? "Desconhecido"; String name = row['player_name']?.toString() ?? "Desconhecido";
if (!aggregated.containsKey(name)) aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0}; if (!aggregated.containsKey(name)) aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
aggregated[name]!['j'] += 1; aggregated[name]!['pts'] += (row['pts'] ?? 0); aggregated[name]!['ast'] += (row['ast'] ?? 0);
aggregated[name]!['j'] += 1; aggregated[name]!['rbs'] += (row['rbs'] ?? 0); aggregated[name]!['stl'] += (row['stl'] ?? 0); aggregated[name]!['blk'] += (row['blk'] ?? 0);
aggregated[name]!['pts'] += (row['pts'] ?? 0);
aggregated[name]!['ast'] += (row['ast'] ?? 0);
aggregated[name]!['rbs'] += (row['rbs'] ?? 0);
aggregated[name]!['stl'] += (row['stl'] ?? 0);
aggregated[name]!['blk'] += (row['blk'] ?? 0);
} }
for (var game in games) { for (var game in games) {
String? mvp = game['mvp_name']; String? mvp = game['mvp_name']; String? defRaw = game['top_def_name'];
String? defRaw = game['top_def_name'];
if (mvp != null && aggregated.containsKey(mvp)) aggregated[mvp]!['mvp'] += 1; if (mvp != null && aggregated.containsKey(mvp)) aggregated[mvp]!['mvp'] += 1;
if (defRaw != null) { if (defRaw != null) {
String defName = defRaw.split(' (')[0].trim(); String defName = defRaw.split(' (')[0].trim();
@@ -134,92 +181,113 @@ class _StatusPageState extends State<StatusPage> {
return {'name': 'TOTAL EQUIPA', 'j': teamGames, 'pts': tPts, 'ast': tAst, 'rbs': tRbs, 'stl': tStl, 'blk': tBlk, 'mvp': tMvp, 'def': tDef}; return {'name': 'TOTAL EQUIPA', 'j': teamGames, 'pts': tPts, 'ast': tAst, 'rbs': tRbs, 'stl': tStl, 'blk': tBlk, 'mvp': tMvp, 'def': tDef};
} }
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals) { Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, double safeSf, double screenWidth) {
double dynamicSpacing = math.max(15 * safeSf, (screenWidth - (180 * safeSf)) / 8);
return Container( return Container(
color: Colors.white, color: Colors.white,
width: double.infinity,
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: DataTable( child: ConstrainedBox(
columnSpacing: 25 * context.sf, constraints: BoxConstraints(minWidth: screenWidth),
headingRowColor: MaterialStateProperty.all(Colors.grey.shade100), child: DataTable(
dataRowHeight: 60 * context.sf, columnSpacing: dynamicSpacing,
columns: [ horizontalMargin: 20 * safeSf,
DataColumn(label: const Text('JOGADOR')), headingRowColor: MaterialStateProperty.all(Colors.grey.shade100),
_buildSortableColumn(context, 'J', 'j'), dataRowHeight: 60 * safeSf,
_buildSortableColumn(context, 'PTS', 'pts'), columns: [
_buildSortableColumn(context, 'AST', 'ast'), DataColumn(label: const Text('JOGADOR')),
_buildSortableColumn(context, 'RBS', 'rbs'), _buildSortableColumn('J', 'j', safeSf),
_buildSortableColumn(context, 'STL', 'stl'), _buildSortableColumn('PTS', 'pts', safeSf),
_buildSortableColumn(context, 'BLK', 'blk'), _buildSortableColumn('AST', 'ast', safeSf),
_buildSortableColumn(context, 'DEF 🛡️', 'def'), _buildSortableColumn('RBS', 'rbs', safeSf),
_buildSortableColumn(context, 'MVP 🏆', 'mvp'), _buildSortableColumn('STL', 'stl', safeSf),
], _buildSortableColumn('BLK', 'blk', safeSf),
rows: [ _buildSortableColumn('DEF 🛡️', 'def', safeSf),
...players.map((player) => DataRow(cells: [ _buildSortableColumn('MVP 🏆', 'mvp', safeSf),
DataCell(Row(children: [CircleAvatar(radius: 15 * context.sf, backgroundColor: Colors.grey.shade200, child: Icon(Icons.person, size: 18 * context.sf)), SizedBox(width: 10 * context.sf), Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf))])), ],
DataCell(Center(child: Text(player['j'].toString()))), rows: [
_buildStatCell(context, player['pts'], isHighlight: true), ...players.map((player) => DataRow(cells: [
_buildStatCell(context, player['ast']), DataCell(
_buildStatCell(context, player['rbs']), // 👇 TORNEI O NOME CLICÁVEL PARA ABRIR O MAPA
_buildStatCell(context, player['stl']), InkWell(
_buildStatCell(context, player['blk']), onTap: () => _openPlayerHeatmap(player['name']),
_buildStatCell(context, player['def'], isBlue: true), child: Row(children: [
_buildStatCell(context, player['mvp'], isGold: true), CircleAvatar(radius: 15 * safeSf, backgroundColor: Colors.grey.shade200, child: Icon(Icons.person, size: 18 * safeSf)),
])), SizedBox(width: 10 * safeSf),
DataRow( Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * safeSf, color: Colors.blue.shade700))
color: MaterialStateProperty.all(Colors.grey.shade50), ]),
cells: [ )
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.black, fontSize: 12 * context.sf))), ),
DataCell(Center(child: Text(teamTotals['j'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)))), DataCell(Center(child: Text(player['j'].toString()))),
_buildStatCell(context, teamTotals['pts'], isHighlight: true), _buildStatCell(player['pts'], safeSf, isHighlight: true),
_buildStatCell(context, teamTotals['ast']), _buildStatCell(player['ast'], safeSf),
_buildStatCell(context, teamTotals['rbs']), _buildStatCell(player['rbs'], safeSf),
_buildStatCell(context, teamTotals['stl']), _buildStatCell(player['stl'], safeSf),
_buildStatCell(context, teamTotals['blk']), _buildStatCell(player['blk'], safeSf),
_buildStatCell(context, teamTotals['def'], isBlue: true), _buildStatCell(player['def'], safeSf, isBlue: true),
_buildStatCell(context, teamTotals['mvp'], isGold: true), _buildStatCell(player['mvp'], safeSf, isGold: true),
] ])),
) DataRow(
], color: MaterialStateProperty.all(Colors.grey.shade50),
cells: [
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.black, fontSize: 12 * safeSf))),
DataCell(Center(child: Text(teamTotals['j'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)))),
_buildStatCell(teamTotals['pts'], safeSf, isHighlight: true),
_buildStatCell(teamTotals['ast'], safeSf),
_buildStatCell(teamTotals['rbs'], safeSf),
_buildStatCell(teamTotals['stl'], safeSf),
_buildStatCell(teamTotals['blk'], safeSf),
_buildStatCell(teamTotals['def'], safeSf, isBlue: true),
_buildStatCell(teamTotals['mvp'], safeSf, isGold: true),
]
)
],
),
), ),
), ),
), ),
); );
} }
DataColumn _buildSortableColumn(BuildContext context, String title, String sortKey) { // (Outras funções de build continuam igual...)
DataColumn _buildSortableColumn(String title, String sortKey, double safeSf) {
return DataColumn(label: InkWell( return DataColumn(label: InkWell(
onTap: () => setState(() { onTap: () => setState(() {
if (_sortColumn == sortKey) _isAscending = !_isAscending; if (_sortColumn == sortKey) _isAscending = !_isAscending;
else { _sortColumn = sortKey; _isAscending = false; } else { _sortColumn = sortKey; _isAscending = false; }
}), }),
child: Row(children: [ child: Row(
Text(title, style: TextStyle(fontSize: 12 * context.sf, fontWeight: FontWeight.bold)), mainAxisSize: MainAxisSize.min,
if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * context.sf, color: const Color(0xFFE74C3C)), children: [
]), Text(title, style: TextStyle(fontSize: 12 * safeSf, fontWeight: FontWeight.bold)),
if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * safeSf, color: const Color(0xFFE74C3C)),
]
),
)); ));
} }
DataCell _buildStatCell(BuildContext context, int value, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) { DataCell _buildStatCell(int value, double safeSf, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) {
return DataCell(Center(child: Container( return DataCell(Center(child: Container(
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), padding: EdgeInsets.symmetric(horizontal: 8 * safeSf, vertical: 4 * safeSf),
decoration: BoxDecoration(color: isGold && value > 0 ? Colors.amber.withOpacity(0.2) : (isBlue && value > 0 ? Colors.blue.withOpacity(0.1) : Colors.transparent), borderRadius: BorderRadius.circular(6)), decoration: BoxDecoration(color: isGold && value > 0 ? Colors.amber.withOpacity(0.2) : (isBlue && value > 0 ? Colors.blue.withOpacity(0.1) : Colors.transparent), borderRadius: BorderRadius.circular(6)),
child: Text(value == 0 ? "-" : value.toString(), style: TextStyle( child: Text(value == 0 ? "-" : value.toString(), style: TextStyle(
fontWeight: (isHighlight || isGold || isBlue) ? FontWeight.w900 : FontWeight.w600, fontWeight: (isHighlight || isGold || isBlue) ? FontWeight.w900 : FontWeight.w600,
fontSize: 14 * context.sf, color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? Colors.green.shade700 : Colors.black87)) fontSize: 14 * safeSf, color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? Colors.green.shade700 : Colors.black87))
)), )),
))); )));
} }
void _showTeamSelector(BuildContext context) { void _showTeamSelector(BuildContext context, double safeSf) {
showModalBottomSheet(context: context, builder: (context) => StreamBuilder<List<Map<String, dynamic>>>( showModalBottomSheet(context: context, builder: (context) => StreamBuilder<List<Map<String, dynamic>>>(
stream: _teamController.teamsStream, stream: _teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
final teams = snapshot.data ?? []; final teams = snapshot.data ?? [];
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile( return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile(
title: Text(teams[i]['name']), title: Text(teams[i]['name'], style: TextStyle(fontSize: 15 * safeSf)),
onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); }, onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); },
)); ));
}, },

View File

@@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
import 'package:playmaker/screens/team_stats_page.dart'; import 'package:playmaker/screens/team_stats_page.dart';
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../models/team_model.dart'; import '../models/team_model.dart';
import '../utils/size_extension.dart'; // 👇 IMPORTANTE: O TEU NOVO SUPERPODER import '../utils/size_extension.dart';
import 'dart:math' as math;
class TeamsPage extends StatefulWidget { class TeamsPage extends StatefulWidget {
const TeamsPage({super.key}); const TeamsPage({super.key});
@@ -121,7 +122,6 @@ class _TeamsPageState extends State<TeamsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 🔥 OLHA QUE LIMPEZA: Já não precisamos de calcular nada aqui!
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F7FA), backgroundColor: const Color(0xFFF5F7FA),
appBar: AppBar( appBar: AppBar(
@@ -142,7 +142,7 @@ class _TeamsPageState extends State<TeamsPage> {
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'add_team_btn', // 👇 A MÁGICA ESTÁ AQUI! heroTag: 'add_team_btn',
backgroundColor: const Color(0xFFE74C3C), backgroundColor: const Color(0xFFE74C3C),
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf), child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
onPressed: () => _showCreateDialog(context), onPressed: () => _showCreateDialog(context),
@@ -151,30 +151,33 @@ class _TeamsPageState extends State<TeamsPage> {
} }
Widget _buildSearchBar() { Widget _buildSearchBar() {
final double safeSf = math.min(context.sf, 1.15); // Travão para a barra não ficar com margens gigantes
return Padding( return Padding(
padding: EdgeInsets.all(16.0 * context.sf), padding: EdgeInsets.all(16.0 * safeSf),
child: TextField( child: TextField(
controller: _searchController, controller: _searchController,
onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()), onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()),
style: TextStyle(fontSize: 16 * context.sf), style: TextStyle(fontSize: 16 * safeSf),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Pesquisar equipa...', hintText: 'Pesquisar equipa...',
hintStyle: TextStyle(fontSize: 16 * context.sf), hintStyle: TextStyle(fontSize: 16 * safeSf),
prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * context.sf), prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * safeSf),
filled: true, filled: true,
fillColor: Colors.white, fillColor: Colors.white,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * context.sf), borderSide: BorderSide.none), border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * safeSf), borderSide: BorderSide.none),
), ),
), ),
); );
} }
Widget _buildTeamsList() { Widget _buildTeamsList() {
final double safeSf = math.min(context.sf, 1.15);
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: controller.teamsStream, stream: controller.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator()); if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * context.sf))); if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * safeSf)));
var data = List<Map<String, dynamic>>.from(snapshot.data!); var data = List<Map<String, dynamic>>.from(snapshot.data!);
@@ -191,7 +194,7 @@ class _TeamsPageState extends State<TeamsPage> {
}); });
return ListView.builder( return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf), padding: EdgeInsets.symmetric(horizontal: 16 * safeSf), // Margem perfeitamente alinhada
itemCount: data.length, itemCount: data.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final team = Team.fromMap(data[index]); final team = Team.fromMap(data[index]);
@@ -224,68 +227,70 @@ class TeamCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // O verdadeiro salvador do tablet
return Card( return Card(
color: Colors.white, elevation: 3, margin: EdgeInsets.only(bottom: 12 * context.sf), color: Colors.white, elevation: 3, margin: EdgeInsets.only(bottom: 12 * safeSf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * safeSf)),
child: ListTile( child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 8 * context.sf), contentPadding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 8 * safeSf),
leading: Stack( leading: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 28 * context.sf, backgroundColor: Colors.grey[200], radius: 28 * safeSf, backgroundColor: Colors.grey[200],
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http')) ? NetworkImage(team.imageUrl) : null, backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http')) ? NetworkImage(team.imageUrl) : null,
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http')) ? Text(team.imageUrl.isEmpty ? "🏀" : team.imageUrl, style: TextStyle(fontSize: 24 * context.sf)) : null, child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http')) ? Text(team.imageUrl.isEmpty ? "🏀" : team.imageUrl, style: TextStyle(fontSize: 24 * safeSf)) : null,
), ),
Positioned( Positioned(
left: -15 * context.sf, top: -10 * context.sf, left: -15 * safeSf, top: -10 * safeSf,
child: IconButton( child: IconButton(
icon: Icon(team.isFavorite ? Icons.star : Icons.star_border, color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1), size: 28 * context.sf, shadows: [Shadow(color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), blurRadius: 4 * context.sf)]), icon: Icon(team.isFavorite ? Icons.star : Icons.star_border, color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1), size: 28 * safeSf, shadows: [Shadow(color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), blurRadius: 4 * safeSf)]),
onPressed: onFavoriteTap, onPressed: onFavoriteTap,
), ),
), ),
], ],
), ),
title: Text(team.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * context.sf), overflow: TextOverflow.ellipsis), title: Text(team.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * safeSf), overflow: TextOverflow.ellipsis),
subtitle: Padding( subtitle: Padding(
padding: EdgeInsets.only(top: 6.0 * context.sf), padding: EdgeInsets.only(top: 6.0 * safeSf),
child: Row( child: Row(
children: [ children: [
Icon(Icons.groups_outlined, size: 16 * context.sf, color: Colors.grey), Icon(Icons.groups_outlined, size: 16 * safeSf, color: Colors.grey),
SizedBox(width: 4 * context.sf), SizedBox(width: 4 * safeSf),
StreamBuilder<int>( StreamBuilder<int>(
stream: controller.getPlayerCountStream(team.id), stream: controller.getPlayerCountStream(team.id),
initialData: 0, initialData: 0,
builder: (context, snapshot) { builder: (context, snapshot) {
final count = snapshot.data ?? 0; final count = snapshot.data ?? 0;
return Text("$count Jogs.", style: TextStyle(color: count > 0 ? Colors.green[700] : Colors.orange, fontWeight: FontWeight.bold, fontSize: 13 * context.sf)); return Text("$count Jogs.", style: TextStyle(color: count > 0 ? Colors.green[700] : Colors.orange, fontWeight: FontWeight.bold, fontSize: 13 * safeSf));
}, },
), ),
SizedBox(width: 8 * context.sf), SizedBox(width: 8 * safeSf),
Expanded(child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * context.sf), overflow: TextOverflow.ellipsis)), Expanded(child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * safeSf), overflow: TextOverflow.ellipsis)),
], ],
), ),
), ),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton(tooltip: 'Ver Estatísticas', icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * context.sf), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team)))), IconButton(tooltip: 'Ver Estatísticas', icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * safeSf), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team)))),
IconButton(tooltip: 'Eliminar Equipa', icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * context.sf), onPressed: () => _confirmDelete(context)), IconButton(tooltip: 'Eliminar Equipa', icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * safeSf), onPressed: () => _confirmDelete(context, safeSf)),
], ],
), ),
), ),
); );
} }
void _confirmDelete(BuildContext context) { void _confirmDelete(BuildContext context, double safeSf) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)),
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * context.sf)), content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * safeSf)),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))), TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * safeSf))),
TextButton(onPressed: () { controller.deleteTeam(team.id); Navigator.pop(context); }, child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * context.sf))), TextButton(onPressed: () { controller.deleteTeam(team.id); Navigator.pop(context); }, child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * safeSf))),
], ],
), ),
); );
@@ -308,32 +313,37 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15);
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * safeSf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Container(
mainAxisSize: MainAxisSize.min, constraints: BoxConstraints(maxWidth: 450 * safeSf), // O popup pode ter um travão para não cobrir a tela toda, fica mais bonito
children: [ child: Column(
TextField(controller: _nameController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * context.sf)), textCapitalization: TextCapitalization.words), mainAxisSize: MainAxisSize.min,
SizedBox(height: 15 * context.sf), children: [
DropdownButtonFormField<String>( TextField(controller: _nameController, style: TextStyle(fontSize: 14 * safeSf), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * safeSf)), textCapitalization: TextCapitalization.words),
value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf)), SizedBox(height: 15 * safeSf),
style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87), DropdownButtonFormField<String>(
items: ['2023/24', '2024/25', '2025/26'].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * safeSf)),
onChanged: (val) => setState(() => _selectedSeason = val!), style: TextStyle(fontSize: 14 * safeSf, color: Colors.black87),
), items: ['2023/24', '2024/25', '2025/26'].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
SizedBox(height: 15 * context.sf), onChanged: (val) => setState(() => _selectedSeason = val!),
TextField(controller: _imageController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'URL Imagem ou Emoji', labelStyle: TextStyle(fontSize: 14 * context.sf), hintText: 'Ex: 🏀 ou https://...', hintStyle: TextStyle(fontSize: 14 * context.sf))), ),
], SizedBox(height: 15 * safeSf),
TextField(controller: _imageController, style: TextStyle(fontSize: 14 * safeSf), decoration: InputDecoration(labelText: 'URL Imagem ou Emoji', labelStyle: TextStyle(fontSize: 14 * safeSf), hintText: 'Ex: 🏀 ou https://...', hintStyle: TextStyle(fontSize: 14 * safeSf))),
],
),
), ),
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))), TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * safeSf))),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)), style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), padding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 10 * safeSf)),
onPressed: () { if (_nameController.text.trim().isNotEmpty) { widget.onConfirm(_nameController.text.trim(), _selectedSeason, _imageController.text.trim()); Navigator.pop(context); } }, onPressed: () { if (_nameController.text.trim().isNotEmpty) { widget.onConfirm(_nameController.text.trim(), _selectedSeason, _imageController.text.trim()); Navigator.pop(context); } },
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * context.sf)), child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * safeSf)),
), ),
], ],
); );

View File

@@ -1,18 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/controllers/login_controller.dart'; import 'package:playmaker/controllers/login_controller.dart';
import 'package:playmaker/pages/RegisterPage.dart'; import 'package:playmaker/pages/RegisterPage.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER! import '../utils/size_extension.dart';
import 'dart:math' as math; // 👇 IMPORTANTE PARA O TRAVÃO NO TABLET!
class BasketTrackHeader extends StatelessWidget { class BasketTrackHeader extends StatelessWidget {
const BasketTrackHeader({super.key}); const BasketTrackHeader({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // TRAVÃO DE MÃO
return Column( return Column(
children: [ children: [
SizedBox( SizedBox(
width: 200 * context.sf, // Ajusta o tamanho da imagem suavemente width: 200 * safeSf,
height: 200 * context.sf, height: 200 * safeSf,
child: Image.asset( child: Image.asset(
'assets/playmaker-logos.png', 'assets/playmaker-logos.png',
fit: BoxFit.contain, fit: BoxFit.contain,
@@ -21,16 +24,16 @@ class BasketTrackHeader extends StatelessWidget {
Text( Text(
'BasketTrack', 'BasketTrack',
style: TextStyle( style: TextStyle(
fontSize: 36 * context.sf, fontSize: 36 * safeSf,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.grey[900], color: Colors.grey[900],
), ),
), ),
SizedBox(height: 6 * context.sf), SizedBox(height: 6 * safeSf),
Text( Text(
'Gere as tuas equipas e estatísticas', 'Gere as tuas equipas e estatísticas',
style: TextStyle( style: TextStyle(
fontSize: 16 * context.sf, fontSize: 16 * safeSf,
color: Colors.grey[600], color: Colors.grey[600],
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
@@ -48,40 +51,42 @@ class LoginFormFields extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15);
return Column( return Column(
children: [ children: [
TextField( TextField(
controller: controller.emailController, controller: controller.emailController,
style: TextStyle(fontSize: 15 * context.sf), style: TextStyle(fontSize: 15 * safeSf),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'E-mail', labelText: 'E-mail',
labelStyle: TextStyle(fontSize: 15 * context.sf), labelStyle: TextStyle(fontSize: 15 * safeSf),
prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf), prefixIcon: Icon(Icons.email_outlined, size: 22 * safeSf),
errorText: controller.emailError, errorText: controller.emailError,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * safeSf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), contentPadding: EdgeInsets.symmetric(vertical: 18 * safeSf, horizontal: 16 * safeSf),
), ),
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
), ),
SizedBox(height: 20 * context.sf), SizedBox(height: 20 * safeSf),
TextField( TextField(
controller: controller.passwordController, controller: controller.passwordController,
obscureText: controller.obscurePassword, obscureText: controller.obscurePassword,
style: TextStyle(fontSize: 15 * context.sf), style: TextStyle(fontSize: 15 * safeSf),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Palavra-passe', labelText: 'Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * context.sf), labelStyle: TextStyle(fontSize: 15 * safeSf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf), prefixIcon: Icon(Icons.lock_outlined, size: 22 * safeSf),
errorText: controller.passwordError, errorText: controller.passwordError,
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon( icon: Icon(
controller.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, controller.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
size: 22 * context.sf size: 22 * safeSf
), ),
onPressed: controller.togglePasswordVisibility, onPressed: controller.togglePasswordVisibility,
), ),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * safeSf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), contentPadding: EdgeInsets.symmetric(vertical: 18 * safeSf, horizontal: 16 * safeSf),
), ),
), ),
], ],
@@ -97,9 +102,11 @@ class LoginButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15);
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
height: 58 * context.sf, height: 58 * safeSf,
child: ElevatedButton( child: ElevatedButton(
onPressed: controller.isLoading ? null : () async { onPressed: controller.isLoading ? null : () async {
final success = await controller.login(); final success = await controller.login();
@@ -108,15 +115,15 @@ class LoginButton extends StatelessWidget {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C), backgroundColor: const Color(0xFFE74C3C),
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * safeSf)),
elevation: 3, elevation: 3,
), ),
child: controller.isLoading child: controller.isLoading
? SizedBox( ? SizedBox(
width: 28 * context.sf, height: 28 * context.sf, width: 28 * safeSf, height: 28 * safeSf,
child: const CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(Colors.white)), child: const CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(Colors.white)),
) )
: Text('Entrar', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), : Text('Entrar', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)),
), ),
); );
} }
@@ -127,19 +134,21 @@ class CreateAccountButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15);
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
height: 58 * context.sf, height: 58 * safeSf,
child: OutlinedButton( child: OutlinedButton(
onPressed: () { onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterPage())); Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterPage()));
}, },
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFE74C3C), foregroundColor: const Color(0xFFE74C3C),
side: BorderSide(color: const Color(0xFFE74C3C), width: 2 * context.sf), side: BorderSide(color: const Color(0xFFE74C3C), width: 2 * safeSf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * safeSf)),
), ),
child: Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), child: Text('Criar Conta', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)),
), ),
); );
} }

View File

@@ -1,24 +1,27 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../controllers/register_controller.dart'; import '../controllers/register_controller.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER! import '../utils/size_extension.dart';
import 'dart:math' as math; // 👇 IMPORTANTE
class RegisterHeader extends StatelessWidget { class RegisterHeader extends StatelessWidget {
const RegisterHeader({super.key}); const RegisterHeader({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // TRAVÃO
return Column( return Column(
children: [ children: [
Icon(Icons.person_add_outlined, size: 100 * context.sf, color: const Color(0xFFE74C3C)), Icon(Icons.person_add_outlined, size: 100 * safeSf, color: const Color(0xFFE74C3C)),
SizedBox(height: 10 * context.sf), SizedBox(height: 10 * safeSf),
Text( Text(
'Nova Conta', 'Nova Conta',
style: TextStyle(fontSize: 36 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[900]), style: TextStyle(fontSize: 36 * safeSf, fontWeight: FontWeight.bold, color: Colors.grey[900]),
), ),
SizedBox(height: 5 * context.sf), SizedBox(height: 5 * safeSf),
Text( Text(
'Cria o teu perfil no BasketTrack', 'Cria o teu perfil no BasketTrack',
style: TextStyle(fontSize: 16 * context.sf, color: Colors.grey[600], fontWeight: FontWeight.w500), style: TextStyle(fontSize: 16 * safeSf, color: Colors.grey[600], fontWeight: FontWeight.w500),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
], ],
@@ -39,71 +42,76 @@ class _RegisterFormFieldsState extends State<RegisterFormFields> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Form( final double safeSf = math.min(context.sf, 1.15); // TRAVÃO
key: widget.controller.formKey,
child: Column(
children: [
TextFormField(
controller: widget.controller.nameController,
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'Nome Completo',
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.person_outline, size: 22 * context.sf),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
),
),
SizedBox(height: 20 * context.sf),
TextFormField( return Container(
controller: widget.controller.emailController, constraints: BoxConstraints(maxWidth: 450 * safeSf), // LIMITA A LARGURA NO TABLET
validator: widget.controller.validateEmail, child: Form(
style: TextStyle(fontSize: 15 * context.sf), key: widget.controller.formKey,
decoration: InputDecoration( child: Column(
labelText: 'E-mail', children: [
labelStyle: TextStyle(fontSize: 15 * context.sf), TextFormField(
prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf), controller: widget.controller.nameController,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), style: TextStyle(fontSize: 15 * safeSf),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), decoration: InputDecoration(
), labelText: 'Nome Completo',
keyboardType: TextInputType.emailAddress, labelStyle: TextStyle(fontSize: 15 * safeSf),
), prefixIcon: Icon(Icons.person_outline, size: 22 * safeSf),
SizedBox(height: 20 * context.sf), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * safeSf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * safeSf, horizontal: 16 * safeSf),
TextFormField(
controller: widget.controller.passwordController,
obscureText: _obscurePassword,
validator: widget.controller.validatePassword,
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf),
suffixIcon: IconButton(
icon: Icon(_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 22 * context.sf),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
), ),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
), ),
), SizedBox(height: 20 * safeSf),
SizedBox(height: 20 * context.sf),
TextFormField( TextFormField(
controller: widget.controller.confirmPasswordController, controller: widget.controller.emailController,
obscureText: _obscurePassword, validator: widget.controller.validateEmail,
validator: widget.controller.validateConfirmPassword, style: TextStyle(fontSize: 15 * safeSf),
style: TextStyle(fontSize: 15 * context.sf), decoration: InputDecoration(
decoration: InputDecoration( labelText: 'E-mail',
labelText: 'Confirmar Palavra-passe', labelStyle: TextStyle(fontSize: 15 * safeSf),
labelStyle: TextStyle(fontSize: 15 * context.sf), prefixIcon: Icon(Icons.email_outlined, size: 22 * safeSf),
prefixIcon: Icon(Icons.lock_clock_outlined, size: 22 * context.sf), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * safeSf)),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), contentPadding: EdgeInsets.symmetric(vertical: 18 * safeSf, horizontal: 16 * safeSf),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), ),
keyboardType: TextInputType.emailAddress,
), ),
), SizedBox(height: 20 * safeSf),
],
TextFormField(
controller: widget.controller.passwordController,
obscureText: _obscurePassword,
validator: widget.controller.validatePassword,
style: TextStyle(fontSize: 15 * safeSf),
decoration: InputDecoration(
labelText: 'Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * safeSf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * safeSf),
suffixIcon: IconButton(
icon: Icon(_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 22 * safeSf),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * safeSf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * safeSf, horizontal: 16 * safeSf),
),
),
SizedBox(height: 20 * safeSf),
TextFormField(
controller: widget.controller.confirmPasswordController,
obscureText: _obscurePassword,
validator: widget.controller.validateConfirmPassword,
style: TextStyle(fontSize: 15 * safeSf),
decoration: InputDecoration(
labelText: 'Confirmar Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * safeSf),
prefixIcon: Icon(Icons.lock_clock_outlined, size: 22 * safeSf),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * safeSf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * safeSf, horizontal: 16 * safeSf),
),
),
],
),
), ),
); );
} }
@@ -115,23 +123,25 @@ class RegisterButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( final double safeSf = math.min(context.sf, 1.15); // TRAVÃO
width: double.infinity,
height: 58 * context.sf, return Container(
constraints: BoxConstraints(maxWidth: 450 * safeSf), // LIMITA LARGURA
height: 58 * safeSf,
child: ElevatedButton( child: ElevatedButton(
onPressed: controller.isLoading ? null : () => controller.signUp(context), onPressed: controller.isLoading ? null : () => controller.signUp(context),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C), backgroundColor: const Color(0xFFE74C3C),
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * safeSf)),
elevation: 3, elevation: 3,
), ),
child: controller.isLoading child: controller.isLoading
? SizedBox( ? SizedBox(
width: 28 * context.sf, height: 28 * context.sf, width: 28 * safeSf, height: 28 * safeSf,
child: const CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(Colors.white)), child: const CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(Colors.white)),
) )
: Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), : Text('Criar Conta', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)),
), ),
); );
} }

View File

@@ -2,12 +2,13 @@ import 'package:flutter/material.dart';
import 'package:playmaker/screens/team_stats_page.dart'; import 'package:playmaker/screens/team_stats_page.dart';
import '../models/team_model.dart'; import '../models/team_model.dart';
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import 'dart:math' as math; // 👇 IMPORTANTE PARA O TRAVÃO DE MÃO
class TeamCard extends StatelessWidget { class TeamCard extends StatelessWidget {
final Team team; final Team team;
final TeamController controller; final TeamController controller;
final VoidCallback onFavoriteTap; final VoidCallback onFavoriteTap;
final double sf; // <-- Variável de escala final double sf; // <-- Variável de escala original
const TeamCard({ const TeamCard({
super.key, super.key,
@@ -19,20 +20,24 @@ class TeamCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 👇 O SEGREDO ESTÁ AQUI: TRAVÃO DE MÃO PARA TABLETS 👇
// O sf pode crescer, mas NUNCA vai ser maior que 1.15!
final double safeSf = math.min(sf, 1.15);
return Card( return Card(
color: Colors.white, color: Colors.white,
elevation: 3, elevation: 3,
margin: EdgeInsets.only(bottom: 12 * sf), margin: EdgeInsets.only(bottom: 12 * safeSf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * safeSf)),
child: ListTile( child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf), contentPadding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 8 * safeSf),
// --- 1. IMAGEM + FAVORITO --- // --- 1. IMAGEM + FAVORITO ---
leading: Stack( leading: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 28 * sf, radius: 28 * safeSf,
backgroundColor: Colors.grey[200], backgroundColor: Colors.grey[200],
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http')) backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
? NetworkImage(team.imageUrl) ? NetworkImage(team.imageUrl)
@@ -40,22 +45,22 @@ class TeamCard extends StatelessWidget {
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http')) child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
? Text( ? Text(
team.imageUrl.isEmpty ? "🏀" : team.imageUrl, team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
style: TextStyle(fontSize: 24 * sf), style: TextStyle(fontSize: 24 * safeSf),
) )
: null, : null,
), ),
Positioned( Positioned(
left: -15 * sf, left: -15 * safeSf,
top: -10 * sf, top: -10 * safeSf,
child: IconButton( child: IconButton(
icon: Icon( icon: Icon(
team.isFavorite ? Icons.star : Icons.star_border, team.isFavorite ? Icons.star : Icons.star_border,
color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1), color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1),
size: 28 * sf, size: 28 * safeSf,
shadows: [ shadows: [
Shadow( Shadow(
color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1),
blurRadius: 4 * sf, blurRadius: 4 * safeSf,
), ),
], ],
), ),
@@ -68,40 +73,39 @@ class TeamCard extends StatelessWidget {
// --- 2. TÍTULO --- // --- 2. TÍTULO ---
title: Text( title: Text(
team.name, team.name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * safeSf),
overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos overflow: TextOverflow.ellipsis,
), ),
// --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) --- // --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) ---
subtitle: Padding( subtitle: Padding(
padding: EdgeInsets.only(top: 6.0 * sf), padding: EdgeInsets.only(top: 6.0 * safeSf),
child: Row( child: Row(
children: [ children: [
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey), Icon(Icons.groups_outlined, size: 16 * safeSf, color: Colors.grey),
SizedBox(width: 4 * sf), SizedBox(width: 4 * safeSf),
// 👇 A CORREÇÃO ESTÁ AQUI: StreamBuilder em vez de FutureBuilder 👇
StreamBuilder<int>( StreamBuilder<int>(
stream: controller.getPlayerCountStream(team.id), stream: controller.getPlayerCountStream(team.id),
initialData: 0, initialData: 0,
builder: (context, snapshot) { builder: (context, snapshot) {
final count = snapshot.data ?? 0; final count = snapshot.data ?? 0;
return Text( return Text(
"$count Jogs.", // Abreviado para poupar espaço "$count Jogs.",
style: TextStyle( style: TextStyle(
color: count > 0 ? Colors.green[700] : Colors.orange, color: count > 0 ? Colors.green[700] : Colors.orange,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 13 * sf, fontSize: 13 * safeSf,
), ),
); );
}, },
), ),
SizedBox(width: 8 * sf), SizedBox(width: 8 * safeSf),
Expanded( // Garante que a temporada se adapta se faltar espaço Expanded(
child: Text( child: Text(
"| ${team.season}", "| ${team.season}",
style: TextStyle(color: Colors.grey, fontSize: 13 * sf), style: TextStyle(color: Colors.grey, fontSize: 13 * safeSf),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@@ -111,11 +115,11 @@ class TeamCard extends StatelessWidget {
// --- 4. BOTÕES (Estatísticas e Apagar) --- // --- 4. BOTÕES (Estatísticas e Apagar) ---
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, // <-- ISTO RESOLVE O OVERFLOW DAS RISCAS AMARELAS mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( IconButton(
tooltip: 'Ver Estatísticas', tooltip: 'Ver Estatísticas',
icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * sf), icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * safeSf),
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
@@ -127,8 +131,8 @@ class TeamCard extends StatelessWidget {
), ),
IconButton( IconButton(
tooltip: 'Eliminar Equipa', tooltip: 'Eliminar Equipa',
icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * sf), icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * safeSf),
onPressed: () => _confirmDelete(context), onPressed: () => _confirmDelete(context, safeSf),
), ),
], ],
), ),
@@ -136,23 +140,23 @@ class TeamCard extends StatelessWidget {
); );
} }
void _confirmDelete(BuildContext context) { void _confirmDelete(BuildContext context, double safeSf) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold)), title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)),
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * sf)), content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * safeSf)),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf)), child: Text('Cancelar', style: TextStyle(fontSize: 14 * safeSf)),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
controller.deleteTeam(team.id); controller.deleteTeam(team.id);
Navigator.pop(context); Navigator.pop(context);
}, },
child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * sf)), child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * safeSf)),
), ),
], ],
), ),
@@ -163,7 +167,7 @@ class TeamCard extends StatelessWidget {
// --- DIALOG DE CRIAÇÃO --- // --- DIALOG DE CRIAÇÃO ---
class CreateTeamDialog extends StatefulWidget { class CreateTeamDialog extends StatefulWidget {
final Function(String name, String season, String imageUrl) onConfirm; final Function(String name, String season, String imageUrl) onConfirm;
final double sf; // Recebe a escala final double sf;
const CreateTeamDialog({super.key, required this.onConfirm, required this.sf}); const CreateTeamDialog({super.key, required this.onConfirm, required this.sf});
@@ -178,58 +182,65 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 👇 MESMO TRAVÃO NO POPUP PARA NÃO FICAR GIGANTE 👇
final double safeSf = math.min(widget.sf, 1.15);
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * safeSf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold)), title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Container(
mainAxisSize: MainAxisSize.min, // 👇 Limita a largura máxima no tablet para o popup não ficar super esticado!
children: [ constraints: BoxConstraints(maxWidth: 450 * safeSf),
TextField( child: Column(
controller: _nameController, mainAxisSize: MainAxisSize.min,
style: TextStyle(fontSize: 14 * widget.sf), children: [
decoration: InputDecoration( TextField(
labelText: 'Nome da Equipa', controller: _nameController,
labelStyle: TextStyle(fontSize: 14 * widget.sf) style: TextStyle(fontSize: 14 * safeSf),
decoration: InputDecoration(
labelText: 'Nome da Equipa',
labelStyle: TextStyle(fontSize: 14 * safeSf)
),
textCapitalization: TextCapitalization.words,
), ),
textCapitalization: TextCapitalization.words, SizedBox(height: 15 * safeSf),
), DropdownButtonFormField<String>(
SizedBox(height: 15 * widget.sf), value: _selectedSeason,
DropdownButtonFormField<String>( decoration: InputDecoration(
value: _selectedSeason, labelText: 'Temporada',
decoration: InputDecoration( labelStyle: TextStyle(fontSize: 14 * safeSf)
labelText: 'Temporada', ),
labelStyle: TextStyle(fontSize: 14 * widget.sf) style: TextStyle(fontSize: 14 * safeSf, color: Colors.black87),
items: ['2023/24', '2024/25', '2025/26']
.map((s) => DropdownMenuItem(value: s, child: Text(s)))
.toList(),
onChanged: (val) => setState(() => _selectedSeason = val!),
), ),
style: TextStyle(fontSize: 14 * widget.sf, color: Colors.black87), SizedBox(height: 15 * safeSf),
items: ['2023/24', '2024/25', '2025/26'] TextField(
.map((s) => DropdownMenuItem(value: s, child: Text(s))) controller: _imageController,
.toList(), style: TextStyle(fontSize: 14 * safeSf),
onChanged: (val) => setState(() => _selectedSeason = val!), decoration: InputDecoration(
), labelText: 'URL Imagem ou Emoji',
SizedBox(height: 15 * widget.sf), labelStyle: TextStyle(fontSize: 14 * safeSf),
TextField( hintText: 'Ex: 🏀 ou https://...',
controller: _imageController, hintStyle: TextStyle(fontSize: 14 * safeSf)
style: TextStyle(fontSize: 14 * widget.sf), ),
decoration: InputDecoration(
labelText: 'URL Imagem ou Emoji',
labelStyle: TextStyle(fontSize: 14 * widget.sf),
hintText: 'Ex: 🏀 ou https://...',
hintStyle: TextStyle(fontSize: 14 * widget.sf)
), ),
), ],
], ),
), ),
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf)) child: Text('Cancelar', style: TextStyle(fontSize: 14 * safeSf))
), ),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C), backgroundColor: const Color(0xFFE74C3C),
padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf) padding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 10 * safeSf)
), ),
onPressed: () { onPressed: () {
if (_nameController.text.trim().isNotEmpty) { if (_nameController.text.trim().isNotEmpty) {
@@ -241,7 +252,7 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
Navigator.pop(context); Navigator.pop(context);
} }
}, },
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)), child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * safeSf)),
), ),
], ],
); );