Compare commits

...

2 Commits

Author SHA1 Message Date
Diogo
1917b5fe10 Merge branch 'main' of https://git.epvc.pt/230404/PlayMaker 2026-03-13 18:44:18 +00:00
Diogo
142f088763 mjn 2026-03-13 14:57:46 +00:00
13 changed files with 1134 additions and 1128 deletions

208
lib/calibrador_page.dart Normal file
View File

@@ -0,0 +1,208 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:math' as math;
class CalibradorPage extends StatefulWidget {
const CalibradorPage({super.key});
@override
State<CalibradorPage> createState() => _CalibradorPageState();
}
class _CalibradorPageState extends State<CalibradorPage> {
// --- 👇 VALORES INICIAIS 👇 ---
double hoopBaseX = 0.08;
double arcRadius = 0.28;
double cornerY = 0.40;
// -----------------------------------------------------
@override
void initState() {
super.initState();
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeLeft,
]);
}
@override
void dispose() {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
super.dispose();
}
@override
Widget build(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
// O MESMO CÁLCULO EXATO DO PLACAR
final double sf = math.min(wScreen / 1150, hScreen / 720);
return Scaffold(
backgroundColor: const Color(0xFF266174),
body: SafeArea(
top: false,
bottom: false,
child: Stack(
children: [
// 👇 1. O CAMPO COM AS MARGENS EXATAS DO PLACAR 👇
Container(
margin: EdgeInsets.only(left: 65 * sf, right: 65 * sf, bottom: 55 * sf),
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2.5),
image: const DecorationImage(
image: AssetImage('assets/campo.png'),
fit: BoxFit.fill,
),
),
child: LayoutBuilder(
builder: (context, constraints) {
return CustomPaint(
painter: LinePainter(
hoopBaseX: hoopBaseX,
arcRadius: arcRadius,
cornerY: cornerY,
color: Colors.redAccent,
width: constraints.maxWidth,
height: constraints.maxHeight,
),
);
},
),
),
// 👇 2. TOPO: MOSTRADORES DE VALORES COM FITTEDBOX (Não transborda) 👇
Positioned(
top: 0, left: 0, right: 0,
child: Container(
color: Colors.black87.withOpacity(0.8),
padding: EdgeInsets.symmetric(vertical: 5 * sf, horizontal: 15 * sf),
child: FittedBox( // Isto impede o ecrã de dar o erro dos 179 pixels!
fit: BoxFit.scaleDown,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildValueDisplay("Aro X", hoopBaseX, sf),
SizedBox(width: 20 * sf),
_buildValueDisplay("Raio", arcRadius, sf),
SizedBox(width: 20 * sf),
_buildValueDisplay("Canto", cornerY, sf),
SizedBox(width: 30 * sf),
ElevatedButton.icon(
onPressed: () => Navigator.pop(context),
icon: Icon(Icons.check, size: 18 * sf),
label: Text("FECHAR", style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold)),
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
)
],
),
),
),
),
// 👇 3. FUNDO: SLIDERS (Com altura fixa para não dar o erro "hasSize") 👇
Positioned(
bottom: 0, left: 0, right: 0,
child: Container(
color: Colors.black87.withOpacity(0.8),
height: 80 * sf, // Altura segura para os sliders
child: Row(
children: [
Expanded(child: _buildSlider("Pos. do Aro", hoopBaseX, 0.0, 0.25, (val) => setState(() => hoopBaseX = val), sf)),
Expanded(child: _buildSlider("Tam. da Curva", arcRadius, 0.1, 0.5, (val) => setState(() => arcRadius = val), sf)),
Expanded(child: _buildSlider("Pos. do Canto", cornerY, 0.2, 0.5, (val) => setState(() => cornerY = val), sf)),
],
),
),
),
],
),
),
);
}
Widget _buildValueDisplay(String label, double value, double sf) {
return Row(
children: [
Text("$label: ", style: TextStyle(color: Colors.white70, fontSize: 16 * sf)),
Text(value.toStringAsFixed(3), style: TextStyle(color: Colors.yellow, fontSize: 20 * sf, fontWeight: FontWeight.bold)),
],
);
}
Widget _buildSlider(String label, double value, double min, double max, ValueChanged<double> onChanged, double sf) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(label, style: TextStyle(color: Colors.white, fontSize: 12 * sf)),
SizedBox(
height: 40 * sf, // Altura exata para o Slider não crashar
child: Slider(
value: value, min: min, max: max,
activeColor: Colors.yellow, inactiveColor: Colors.white24,
onChanged: onChanged,
),
),
],
);
}
}
// ==============================================================
// 📐 PINTOR: DESENHA A LINHA MATEMÁTICA NA TELA
// ==============================================================
class LinePainter extends CustomPainter {
final double hoopBaseX;
final double arcRadius;
final double cornerY;
final Color color;
final double width;
final double height;
LinePainter({
required this.hoopBaseX, required this.arcRadius, required this.cornerY,
required this.color, required this.width, required this.height,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 4;
double aspectRatio = width / height;
double hoopY = 0.50 * height;
// O cornerY controla a que distância do meio (50%) estão as linhas retas
double cornerDistY = cornerY * height;
// --- CESTO ESQUERDO ---
double hoopLX = hoopBaseX * width;
canvas.drawLine(Offset(0, hoopY - cornerDistY), Offset(width * 0.35, hoopY - cornerDistY), paint); // Cima
canvas.drawLine(Offset(0, hoopY + cornerDistY), Offset(width * 0.35, hoopY + cornerDistY), paint); // Baixo
canvas.drawArc(
Rect.fromCenter(center: Offset(hoopLX, hoopY), width: arcRadius * width * 2 / aspectRatio, height: arcRadius * height * 2),
-math.pi / 2, math.pi, false, paint,
);
// --- CESTO DIREITO ---
double hoopRX = (1.0 - hoopBaseX) * width;
canvas.drawLine(Offset(width, hoopY - cornerDistY), Offset(width * 0.65, hoopY - cornerDistY), paint); // Cima
canvas.drawLine(Offset(width, hoopY + cornerDistY), Offset(width * 0.65, hoopY + cornerDistY), paint); // Baixo
canvas.drawArc(
Rect.fromCenter(center: Offset(hoopRX, hoopY), width: arcRadius * width * 2 / aspectRatio, height: arcRadius * height * 2),
math.pi / 2, math.pi, false, paint,
);
}
@override
bool shouldRepaint(covariant LinePainter oldDelegate) {
return oldDelegate.hoopBaseX != hoopBaseX || oldDelegate.arcRadius != arcRadius || oldDelegate.cornerY != cornerY;
}
}

View File

@@ -7,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; final String playerName;
ShotRecord({ ShotRecord({
required this.relativeX, required this.relativeX,
@@ -32,6 +32,7 @@ class PlacarController {
bool isLoading = true; bool isLoading = true;
bool isSaving = false; bool isSaving = false;
bool gameWasAlreadyFinished = false; bool gameWasAlreadyFinished = false;
int myScore = 0; int myScore = 0;
@@ -66,27 +67,35 @@ class PlacarController {
Timer? timer; Timer? timer;
bool isRunning = false; bool isRunning = false;
// OS TEUS NÚMEROS DE OURO DO TABLET // 👇 VARIÁVEIS DE CALIBRAÇÃO DO CAMPO (OS TEUS NÚMEROS!) 👇
bool isCalibrating = false; bool isCalibrating = false;
double hoopBaseX = 0.000; double hoopBaseX = 0.088;
double arcRadius = 0.500; double arcRadius = 0.459;
double cornerY = 0.443; double cornerY = 0.440;
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(); myBench.clear(); oppCourt.clear(); oppBench.clear(); myCourt.clear();
playerStats.clear(); playerNumbers.clear(); playerDbIds.clear(); myBench.clear();
myFouls = 0; opponentFouls = 0; oppCourt.clear();
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;
@@ -110,10 +119,17 @@ 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] = { "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 }; 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,
};
myFouls += (s['fls'] as int? ?? 0); myFouls += (s['fls'] as int? ?? 0);
} }
} }
@@ -122,28 +138,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] = { "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 }; 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,
};
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();
} }
@@ -153,9 +169,17 @@ 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 };
if (isMyTeam) { if (isCourt) myCourt.add(name); else myBench.add(name); } playerStats[name] = {
else { if (isCourt) oppCourt.add(name); else oppBench.add(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
};
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}) {
@@ -174,12 +198,17 @@ 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; opponentFouls = 0; myTimeoutsUsed = 0; opponentTimeoutsUsed = 0; myFouls = 0;
onUpdate(); opponentFouls = 0;
} 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();
}); });
@@ -189,8 +218,11 @@ class PlacarController {
} }
void useTimeout(bool isOpponent) { void useTimeout(bool isOpponent) {
if (isOpponent) { if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++; } if (isOpponent) {
else { if (myTimeoutsUsed < 3) myTimeoutsUsed++; } if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++;
} else {
if (myTimeoutsUsed < 3) myTimeoutsUsed++;
}
isRunning = false; isRunning = false;
timer?.cancel(); timer?.cancel();
onUpdate(); onUpdate();
@@ -226,6 +258,7 @@ 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_", "");
@@ -235,36 +268,46 @@ 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();
} }
// ============================================================== // =========================================================================
// 🎯 REGISTO DO TOQUE (INTELIGENTE E SILENCIOSO) // 👇 A MÁGICA DOS PONTOS ACONTECE AQUI 👇
// ============================================================== // =========================================================================
void registerShotLocation(BuildContext context, Offset position, Size size) { 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");
// O ÁRBITRO MATEMÁTICO COM AS TUAS VARIÁVEIS CALIBRADAS
if (is3Pt || is2Pt) { if (is3Pt || is2Pt) {
bool isInside2Pts = _validateShotZone(position, size, isOpponent); bool isValid = _validateShotZone(position, size, is3Pt);
// Bloqueio silencioso (sem notificações chamas) // SE A JOGADA FOI NO SÍTIO ERRADO
if ((is2Pt && !isInside2Pts) || (is3Pt && isInside2Pts)) { if (!isValid) {
cancelShotLocation();
return; return; // <-- ESTE RETURN BLOQUEIA A GRAVAÇÃO DO PONTO!
} }
} }
// SE A JOGADA FOI VÁLIDA:
bool isMake = pendingAction!.startsWith("add_pts_"); bool isMake = pendingAction!.startsWith("add_pts_");
double relX = position.dx / size.width; double relX = position.dx / size.width;
double relY = position.dy / size.height; double relY = position.dy / size.height;
String name = pendingPlayer!.replaceAll("player_my_", "").replaceAll("player_opp_", ""); String name = pendingPlayer!.replaceAll("player_my_", "").replaceAll("player_opp_", "");
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;
@@ -273,37 +316,36 @@ class PlacarController {
onUpdate(); onUpdate();
} }
// ============================================================== bool _validateShotZone(Offset position, Size size, bool is3Pt) {
// 📐 MATEMÁTICA PURA: LÓGICA DE MEIO-CAMPO ATACANTE (SOLUÇÃO DIVIDIDA)
// ==============================================================
bool _validateShotZone(Offset position, Size size, bool isOpponent) {
double relX = position.dx / size.width; double relX = position.dx / size.width;
double relY = position.dy / size.height; double relY = position.dy / size.height;
double hX = hoopBaseX; bool isLeftHalf = relX < 0.5;
double radius = arcRadius; double hoopX = isLeftHalf ? hoopBaseX : (1.0 - hoopBaseX);
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 hoopY = 0.50;
double aspectRatio = size.width / size.height; double aspectRatio = size.width / size.height;
double distFromCenterY = (relY - hoopY).abs(); double distFromCenterY = (relY - hoopY).abs();
// Descobre se o toque foi feito na metade atacante daquela equipa bool isInside2Pts;
bool isAttackingHalf = isOpponent ? (relX < 0.5) : (relX > 0.5);
if (isAttackingHalf && distFromCenterY > cY) { // Lógica das laterais (Cantos)
return false; // É 3 pontos (Zona dos Cantos) if (distFromCenterY > cornerY) {
} else { double distToBaseline = isLeftHalf ? relX : (1.0 - relX);
isInside2Pts = distToBaseline <= hoopBaseX;
}
// Lógica da Curva Frontal
else {
double dx = (relX - hoopX) * aspectRatio; double dx = (relX - hoopX) * aspectRatio;
double dy = (relY - hoopY); double dy = (relY - hoopY);
double distanceToHoop = math.sqrt((dx * dx) + (dy * dy)); double distanceToHoop = math.sqrt((dx * dx) + (dy * dy));
return distanceToHoop <= radius; isInside2Pts = distanceToHoop < arcRadius;
} }
if (is3Pt) return !isInside2Pts;
return isInside2Pts;
} }
// 👆 ===================================================================== 👆
void cancelShotLocation() { void cancelShotLocation() {
isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate(); isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate();
@@ -359,7 +401,7 @@ class PlacarController {
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';
String topPtsName = '---'; int maxPts = -1; String topPtsName = '---'; int maxPts = -1;
@@ -374,8 +416,10 @@ class PlacarController {
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;
int mvpScore = pts + ast + rbs + defScore; int mvpScore = pts + ast + rbs + defScore;
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)'; }
@@ -384,32 +428,54 @@ class PlacarController {
}); });
await supabase.from('games').update({ await supabase.from('games').update({
'my_score': myScore, 'opponent_score': opponentScore, 'remaining_seconds': duration.inSeconds, 'my_score': myScore,
'my_timeouts': myTimeoutsUsed, 'opp_timeouts': opponentTimeoutsUsed, 'current_quarter': currentQuarter, 'opponent_score': opponentScore,
'status': newStatus, 'top_pts_name': topPtsName, 'top_ast_name': topAstName, 'top_rbs_name': topRbsName, 'remaining_seconds': duration.inSeconds,
'top_def_name': topDefName, 'mvp_name': mvpName, 'my_timeouts': myTimeoutsUsed,
'opp_timeouts': opponentTimeoutsUsed,
'current_quarter': currentQuarter,
'status': newStatus,
'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);
// 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) { if(t['id'].toString() == myTeamDbId) myTeamUpdate = Map.from(t);
int w = (t['wins'] ?? 0) + (myScore > opponentScore ? 1 : 0); if(t['id'].toString() == oppTeamDbId) oppTeamUpdate = Map.from(t);
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;
} }
// 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];
@@ -421,32 +487,21 @@ class PlacarController {
}); });
} }
}); });
await supabase.from('player_stats').delete().eq('game_id', gameId);
if (batchStats.isNotEmpty) await supabase.from('player_stats').insert(batchStats);
// =============================================== await supabase.from('player_stats').delete().eq('game_id', gameId);
// 🔥 GRAVAR COORDENADAS PARA O HEATMAP if (batchStats.isNotEmpty) {
// =============================================== await supabase.from('player_stats').insert(batchStats);
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('Tudo guardado com Sucesso!'), backgroundColor: Colors.green)); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas e Resultados guardados com Sucesso!'), backgroundColor: Colors.green));
} }
} catch (e) { } catch (e) {
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao guardar: $e'), backgroundColor: Colors.red)); debugPrint("Erro ao gravar estatísticas: $e");
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();
@@ -456,4 +511,4 @@ class PlacarController {
void dispose() { void dispose() {
timer?.cancel(); timer?.cancel();
} }
} }

View File

@@ -1,7 +1,6 @@
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;
@@ -60,25 +59,30 @@ 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(opacity: _animation.value.clamp(0.0, 1.0), child: child), child: Opacity(
// 👇 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: 8, elevation: 4,
shadowColor: Colors.black54, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
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(
color: const Color(0xFF1A222D), borderRadius: BorderRadius.circular(14),
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) {
@@ -86,89 +90,86 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
final double cw = constraints.maxWidth; final double cw = constraints.maxWidth;
return Padding( return Padding(
padding: EdgeInsets.symmetric(horizontal: cw * 0.05, vertical: ch * 0.03), padding: EdgeInsets.all(cw * 0.06),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// --- CABEÇALHO --- // 👇 TÍTULOS UM POUCO MAIS PRESENTES
FittedBox( FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Text(widget.title.toUpperCase(), child: Text(widget.title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), letterSpacing: 1.0)),
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)),
const Expanded(flex: 1, child: SizedBox()), SizedBox(height: ch * 0.03),
// --- MIOLO (GRÁFICO + STATS GIGANTES À ESQUERDA) --- // MEIO (GRÁFICO + ESTATÍSTICAS)
Expanded( Expanded(
flex: 9,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 1. Lado Esquerdo: Donut Chart LIMPO (Sem texto sobreposto) Expanded(
SizedBox( flex: 1,
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(
child: FittedBox( flex: 1,
alignment: Alignment.centerLeft, child: Column(
fit: BoxFit.scaleDown, mainAxisAlignment: MainAxisAlignment.start,
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, children: [
crossAxisAlignment: CrossAxisAlignment.start, _buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, ch),
children: [ _buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.yellow, ch),
_buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.greenAccent, ch, cw), _buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, ch),
_buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.yellowAccent, ch, cw), _buildDynDivider(ch),
_buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.redAccent, ch, cw), _buildDynStatRow("TOT", data.total.toString(), "100", Colors.white, ch),
_buildDynDivider(cw), ],
_buildDynStatRow("TOT", data.total.toString(), "100", Colors.white, ch, cw),
],
),
), ),
), ),
], ],
), ),
), ),
const Expanded(flex: 1, child: SizedBox()), // 👇 RODAPÉ AJUSTADO
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.025), padding: EdgeInsets.symmetric(vertical: ch * 0.035),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.08), color: Colors.white24, // Igual ao fundo do botão detalhes
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(ch * 0.03), // Borda arredondada
), ),
child: FittedBox( child: Center(
fit: BoxFit.scaleDown, child: FittedBox(
child: Row( fit: BoxFit.scaleDown,
mainAxisAlignment: MainAxisAlignment.center, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.center,
Icon(Icons.stars, color: Colors.greenAccent, size: ch * 0.075), children: [
const SizedBox(width: 10), Icon(
Text('WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%', data.victoryPercentage >= 0.5 ? Icons.trending_up : Icons.trending_down,
style: TextStyle( color: Colors.green,
color: Colors.white, size: ch * 0.09
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
)
),
],
),
), ),
), ),
), ),
@@ -182,38 +183,34 @@ 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, double cw) { Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch) {
return Padding( return Padding(
padding: EdgeInsets.symmetric(vertical: ch * 0.005), padding: EdgeInsets.only(bottom: ch * 0.01),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
SizedBox( // Número subiu para 0.10
width: cw * 0.12, 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)))),
child: Column( SizedBox(width: ch * 0.02),
crossAxisAlignment: CrossAxisAlignment.end, Expanded(
mainAxisSize: MainAxisSize.min, flex: 3,
children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Text(label, style: TextStyle(fontSize: ch * 0.035, color: Colors.white60, fontWeight: FontWeight.bold)), Row(children: [
Text('$percent%', style: TextStyle(fontSize: ch * 0.04, color: color, 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)),
], // 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 cw) { Widget _buildDynDivider(double ch) {
return Container( return Container(height: 0.5, color: Colors.white.withOpacity(0.1), margin: EdgeInsets.symmetric(vertical: ch * 0.01));
width: cw * 0.35,
height: 1.5,
color: Colors.white24,
margin: const EdgeInsets.symmetric(vertical: 4)
);
} }
} }

View File

@@ -1,367 +1,325 @@
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/pages/heatmap_page.dart'; import 'package:playmaker/utils/size_extension.dart';
import 'package:playmaker/utils/size_extension.dart'; import 'package:playmaker/widgets/placar_widgets.dart';
import 'package:playmaker/widgets/placar_widgets.dart'; import 'dart:math' as math;
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();
} }
@override class _PlacarPageState extends State<PlacarPage> {
void dispose() { late PlacarController _controller;
_controller.dispose();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
super.dispose();
}
Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top, double sf) { @override
return Positioned( void initState() {
top: top, left: left > 0 ? left : null, right: right > 0 ? right : null, super.initState();
child: Draggable<String>( SystemChrome.setPreferredOrientations([
data: action, DeviceOrientation.landscapeRight,
feedback: Material(color: Colors.transparent, child: CircleAvatar(radius: 30 * sf, backgroundColor: color.withOpacity(0.8), child: Icon(icon, color: Colors.white, size: 30 * sf))), DeviceOrientation.landscapeLeft,
child: Column( ]);
children: [
CircleAvatar(radius: 27 * sf, backgroundColor: color, child: Icon(icon, color: Colors.white, size: 28 * sf)), _controller = PlacarController(
SizedBox(height: 5 * sf), gameId: widget.gameId,
Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 * sf)), myTeam: widget.myTeam,
], 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))));
} }
return Scaffold( @override
backgroundColor: const Color(0xFF266174), void dispose() {
body: SafeArea( _controller.dispose();
top: false, bottom: false, SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
child: IgnorePointer( super.dispose();
ignoring: _controller.isSaving, }
child: Stack(
children: [
// ==========================================
// --- 1. 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),
image: const DecorationImage(image: AssetImage('assets/campo.png'), fit: BoxFit.fill),
),
child: LayoutBuilder(
builder: (context, constraints) {
final w = constraints.maxWidth;
final h = constraints.maxHeight;
return Stack( // --- BOTÕES FLUTUANTES DE FALTA ---
children: [ Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top, double sf) {
Positioned.fill( return Positioned(
child: GestureDetector( top: top,
behavior: HitTestBehavior.opaque, 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: [
CircleAvatar(
radius: 27 * sf,
backgroundColor: color,
child: Icon(icon, color: Colors.white, size: 28 * sf),
),
SizedBox(height: 5 * sf),
Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 * sf)),
],
),
),
);
}
// --- 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));
},
),
],
),
),
);
}
return Scaffold(
backgroundColor: const Color(0xFF266174),
body: SafeArea(
top: false,
bottom: false,
// 👇 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: Stack( child: Container(
children: _controller.matchShots.map((shot) => Positioned( decoration: const BoxDecoration(
left: (shot.relativeX * w) - (9 * sf), image: DecorationImage(
top: (shot.relativeY * h) - (9 * sf), image: AssetImage('assets/campo.png'),
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)), fit: BoxFit.fill,
)).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.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.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.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.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)), 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 --- // --- BOTÕES DE FALTAS ---
if (!_controller.isSelectingShotLocation) ...[ if (!_controller.isSelectingShotLocation) ...[
_buildFloatingFoulBtn("FALTA +", Colors.orange, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31, sf), _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), _buildFloatingFoulBtn("FALTA -", Colors.redAccent, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31, sf),
], ],
// --- BOTÃO PLAY/PAUSE --- // --- BOTÃO PLAY/PAUSE ---
if (!_controller.isSelectingShotLocation) if (!_controller.isSelectingShotLocation)
Positioned( Positioned(
top: (h * 0.36) + (40 * sf), top: (h * 0.32) + (40 * sf),
left: 0, right: 0, left: 0, right: 0,
child: Center( child: Center(
child: GestureDetector( child: GestureDetector(
onTap: () => _controller.toggleTimer(context), onTap: () => _controller.toggleTimer(context),
child: CircleAvatar( child: CircleAvatar(
radius: 68 * sf, radius: 68 * sf,
backgroundColor: Colors.grey.withOpacity(0.5), backgroundColor: Colors.grey.withOpacity(0.5),
child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 58 * sf) 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 ---
if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller, sf: sf)),
Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))), // --- OVERLAY LANÇAMENTO ---
], if (_controller.isSelectingShotLocation)
); 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 LATERAIS ---
// --- 2. O RODAPÉ (BOTÕES DE JOGO) --- // Topo Esquerdo: Guardar e Sair (Botão Único)
// ==========================================
if (!_controller.isSelectingShotLocation)
Positioned( Positioned(
bottom: 60 * sf, top: 50 * sf, left: 12 * sf,
left: 0, child: _buildCornerBtn(
right: 0, heroTag: 'btn_save_exit',
child: ActionButtonsPanel(controller: _controller, sf: sf) 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
// --- 3. BOTÕES LATERAIS --- Positioned(
// ========================================== bottom: 55 * sf, left: 12 * sf,
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); })),
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, 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: [
Text(displayName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? Colors.red : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), if (_controller.showMyBench) BenchPlayersList(controller: _controller, isOpponent: false, sf: sf),
SizedBox(height: 2 * sf), SizedBox(height: 12 * sf),
Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[700], fontWeight: FontWeight.bold)), _buildCornerBtn(heroTag: 'btn_sub_home', icon: Icons.swap_horiz, color: const Color(0xFF1E5BB2), size: cornerBtnSize, onTap: () { _controller.showMyBench = !_controller.showMyBench; _controller.onUpdate(); }),
// Texto de faltas com destaque se estiver em perigo (4 ou 5) SizedBox(height: 12 * sf),
Text("AST: ${stats["ast"]} | REB: ${stats["orb"]! + stats["drb"]!} | FALTAS: $fouls", _buildCornerBtn(
style: TextStyle( heroTag: 'btn_to_home',
fontSize: 11 * sf, icon: Icons.timer,
color: fouls >= 4 ? Colors.red : Colors.grey[600], color: _controller.myTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFF1E5BB2),
fontWeight: fouls >= 4 ? FontWeight.w900 : FontWeight.w600 size: cornerBtnSize,
)), 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,10 +1,9 @@
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'; import '../utils/size_extension.dart'; // 👇 NOVO SUPERPODER AQUI TAMBÉM!
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 {
@@ -19,61 +18,59 @@ 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 * safeSf), margin: EdgeInsets.only(bottom: 16 * context.sf),
padding: EdgeInsets.all(16 * safeSf), padding: EdgeInsets.all(16 * context.sf),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * safeSf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * safeSf)]), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * context.sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * context.sf)]),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, safeSf)), Expanded(child: _buildTeamInfo(context, myTeam, const Color(0xFFE74C3C), myTeamLogo)),
_buildScoreCenter(context, gameId, safeSf), _buildScoreCenter(context, gameId),
Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, safeSf)), Expanded(child: _buildTeamInfo(context, opponentTeam, Colors.black87, opponentTeamLogo)),
], ],
), ),
); );
} }
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double safeSf) { Widget _buildTeamInfo(BuildContext context, String name, Color color, String? logoUrl) {
return Column( return Column(
children: [ children: [
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), 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),
SizedBox(height: 6 * safeSf), SizedBox(height: 6 * context.sf),
Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * safeSf), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2), Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2),
], ],
); );
} }
Widget _buildScoreCenter(BuildContext context, String id, double safeSf) { Widget _buildScoreCenter(BuildContext context, String id) {
return Column( return Column(
children: [ children: [
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_scoreBox(myScore, Colors.green, safeSf), _scoreBox(context, myScore, Colors.green),
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * safeSf)), Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * context.sf)),
_scoreBox(opponentScore, Colors.grey, safeSf), _scoreBox(context, opponentScore, Colors.grey),
], ],
), ),
SizedBox(height: 10 * safeSf), SizedBox(height: 10 * context.sf),
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 * safeSf, color: const Color(0xFFE74C3C)), icon: Icon(Icons.play_circle_fill, size: 18 * context.sf, color: const Color(0xFFE74C3C)),
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * safeSf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)), label: Text("RETORNAR", style: TextStyle(fontSize: 11 * context.sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
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), 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),
), ),
SizedBox(height: 6 * safeSf), SizedBox(height: 6 * context.sf),
Text(status, style: TextStyle(fontSize: 12 * safeSf, color: Colors.blue, fontWeight: FontWeight.bold)), Text(status, style: TextStyle(fontSize: 12 * context.sf, color: Colors.blue, fontWeight: FontWeight.bold)),
], ],
); );
} }
Widget _scoreBox(String pts, Color c, double safeSf) => Container( Widget _scoreBox(BuildContext context, String pts, Color c) => Container(
padding: EdgeInsets.symmetric(horizontal: 12 * safeSf, vertical: 6 * safeSf), padding: EdgeInsets.symmetric(horizontal: 12 * context.sf, vertical: 6 * context.sf),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * safeSf)), decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * context.sf)),
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * safeSf)), child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)),
); );
} }
@@ -107,30 +104,25 @@ 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 * safeSf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * safeSf)), title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Container( child: Column(
constraints: BoxConstraints(maxWidth: 450 * safeSf), // LIMITA A LARGURA NO TABLET mainAxisSize: MainAxisSize.min,
child: Column( children: [
mainAxisSize: MainAxisSize.min, 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))),
children: [ SizedBox(height: 15 * context.sf),
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))), _buildSearch(context, "Minha Equipa", _myTeamController),
SizedBox(height: 15 * safeSf), Padding(padding: EdgeInsets.symmetric(vertical: 10 * context.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * context.sf))),
_buildSearch(label: "Minha Equipa", controller: _myTeamController, safeSf: safeSf), _buildSearch(context, "Adversário", _opponentController),
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 * safeSf))), TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * context.sf))),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * safeSf)), padding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 10 * safeSf)), 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)),
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);
@@ -142,13 +134,13 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
} }
} }
}, },
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)), 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)),
), ),
], ],
); );
} }
Widget _buildSearch({required String label, required TextEditingController controller, required double safeSf}) { Widget _buildSearch(BuildContext context, String label, TextEditingController controller) {
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) {
@@ -164,9 +156,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 * safeSf), elevation: 4.0, borderRadius: BorderRadius.circular(8 * context.sf),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 250 * safeSf, maxWidth: 400 * safeSf), // Limita também o dropdown constraints: BoxConstraints(maxHeight: 250 * context.sf, maxWidth: MediaQuery.of(context).size.width * 0.7),
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) {
@@ -174,8 +166,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 * 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), 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),
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * safeSf)), title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * context.sf)),
onTap: () { onSelected(option); }, onTap: () { onSelected(option); },
); );
}, },
@@ -188,8 +180,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 * safeSf), controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * context.sf),
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * safeSf), prefixIcon: Icon(Icons.search, size: 20 * safeSf), border: const OutlineInputBorder()), decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * context.sf), prefixIcon: Icon(Icons.search, size: 20 * context.sf), border: const OutlineInputBorder()),
); );
}, },
); );
@@ -198,8 +190,6 @@ 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

@@ -1,92 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../controllers/placar_controller.dart'; // Ajusta o caminho se for preciso
class HeatmapPage extends StatefulWidget {
final List<ShotRecord> shots;
final String teamName;
const HeatmapPage({super.key, required this.shots, required this.teamName});
@override
State<HeatmapPage> createState() => _HeatmapPageState();
}
class _HeatmapPageState extends State<HeatmapPage> {
@override
void initState() {
super.initState();
// Força o ecrã a ficar deitado para vermos bem o campo
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeLeft,
]);
}
@override
void dispose() {
// Volta ao normal quando saímos
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF16202C),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text("Mapa de Lançamentos - ${widget.teamName}", style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
iconTheme: const IconThemeData(color: Colors.white),
),
body: Center(
child: AspectRatio(
aspectRatio: 1150 / 720, // Mantém o campo proporcional
child: Container(
margin: const EdgeInsets.only(bottom: 20, left: 20, right: 20),
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 3),
image: const DecorationImage(
image: AssetImage('assets/campo.png'),
fit: BoxFit.fill,
),
),
child: LayoutBuilder(
builder: (context, constraints) {
final double w = constraints.maxWidth;
final double h = constraints.maxHeight;
return Stack(
children: widget.shots.map((shot) {
// 👇 Converte de volta de % para Pixels reais do ecrã atual
double pixelX = shot.relativeX * w;
double pixelY = shot.relativeY * h;
return Positioned(
left: pixelX - 12, // -12 para centrar a bolinha
top: pixelY - 12,
child: Tooltip(
message: "${shot.playerName}\n${shot.isMake ? 'Cesto' : 'Falha'}",
child: CircleAvatar(
radius: 12,
backgroundColor: shot.isMake ? Colors.green.withOpacity(0.85) : Colors.red.withOpacity(0.85),
child: Icon(
shot.isMake ? Icons.check : Icons.close,
size: 14,
color: Colors.white,
),
),
),
);
}).toList(),
);
},
),
),
),
),
);
}
}

View File

@@ -8,7 +8,6 @@ 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});
@@ -31,10 +30,10 @@ class _HomeScreenState extends State<HomeScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // TRAVÃO // Já não precisamos calcular o sf aqui!
final List<Widget> pages = [ final List<Widget> pages = [
_buildHomeContent(context, safeSf), // Passamos o safeSf _buildHomeContent(context), // Passamos só o context
const GamePage(), const GamePage(),
const TeamsPage(), const TeamsPage(),
const StatusPage(), const StatusPage(),
@@ -43,11 +42,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 * safeSf)), title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)),
backgroundColor: HomeConfig.primaryColor, backgroundColor: HomeConfig.primaryColor,
foregroundColor: Colors.white, foregroundColor: Colors.white,
leading: IconButton( leading: IconButton(
icon: Icon(Icons.person, size: 24 * safeSf), icon: Icon(Icons.person, size: 24 * context.sf),
onPressed: () {}, onPressed: () {},
), ),
), ),
@@ -63,7 +62,8 @@ 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,
height: 70 * safeSf, // O math.min não é necessário se já tens o sf. Mas podes usar context.sf
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, double safeSf) { void _showTeamSelector(BuildContext context) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * safeSf))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf))),
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 * safeSf, child: const Center(child: Text("Nenhuma equipa criada."))); if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * context.sf, 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'], style: TextStyle(fontSize: 16 * safeSf)), title: Text(team['name']),
onTap: () { onTap: () {
setState(() { setState(() {
_selectedTeamId = team['id']; _selectedTeamId = team['id'];
@@ -112,10 +112,9 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
Widget _buildHomeContent(BuildContext context, double safeSf) { Widget _buildHomeContent(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width; final double wScreen = MediaQuery.of(context).size.width;
// Evita que os cartões fiquem muito altos no tablet: final double cardHeight = wScreen * 0.5;
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
@@ -126,44 +125,44 @@ class _HomeScreenState extends State<HomeScreen> {
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: 22.0 * safeSf, vertical: 16.0 * safeSf), padding: EdgeInsets.symmetric(horizontal: 22.0 * context.sf, vertical: 16.0 * context.sf),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
InkWell( InkWell(
onTap: () => _showTeamSelector(context, safeSf), onTap: () => _showTeamSelector(context),
child: Container( child: Container(
padding: EdgeInsets.all(12 * safeSf), padding: EdgeInsets.all(12 * context.sf),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * safeSf), border: Border.all(color: Colors.grey.shade300)), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * context.sf), 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 * safeSf), SizedBox(width: 10 * safeSf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * safeSf, fontWeight: FontWeight.bold))]), 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))]),
const Icon(Icons.arrow_drop_down), const Icon(Icons.arrow_drop_down),
], ],
), ),
), ),
), ),
SizedBox(height: 20 * safeSf), SizedBox(height: 20 * context.sf),
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 * safeSf), SizedBox(width: 12 * context.sf),
Expanded(child: _buildStatCard(context: context, title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: 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 * safeSf), SizedBox(height: 12 * context.sf),
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 * safeSf), SizedBox(width: 12 * context.sf),
Expanded( Expanded(
child: PieChartCard( child: PieChartCard(
victories: _teamWins, victories: _teamWins,
@@ -172,22 +171,22 @@ class _HomeScreenState extends State<HomeScreen> {
title: 'DESEMPENHO', title: 'DESEMPENHO',
subtitle: 'Temporada', subtitle: 'Temporada',
backgroundColor: const Color(0xFFC62828), backgroundColor: const Color(0xFFC62828),
sf: safeSf sf: context.sf // Aqui o PieChartCard ainda usa sf, então passamos
), ),
), ),
], ],
), ),
), ),
SizedBox(height: 40 * safeSf), SizedBox(height: 40 * context.sf),
Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * safeSf, fontWeight: FontWeight.bold, color: Colors.grey[800])), Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[800])),
SizedBox(height: 16 * safeSf), SizedBox(height: 16 * context.sf),
_selectedTeamName == "Selecionar Equipa" _selectedTeamName == "Selecionar Equipa"
? Container( ? Container(
padding: EdgeInsets.all(20 * safeSf), padding: EdgeInsets.all(20 * context.sf),
alignment: Alignment.center, alignment: Alignment.center,
child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * safeSf)), child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)),
) )
: StreamBuilder<List<Map<String, dynamic>>>( : StreamBuilder<List<Map<String, dynamic>>>(
stream: _supabase.from('games').stream(primaryKey: ['id']) stream: _supabase.from('games').stream(primaryKey: ['id'])
@@ -207,7 +206,7 @@ class _HomeScreenState extends State<HomeScreen> {
if (gamesList.isEmpty) { if (gamesList.isEmpty) {
return Container( return Container(
padding: EdgeInsets.all(20 * safeSf), padding: EdgeInsets.all(20 * context.sf),
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)),
@@ -237,7 +236,7 @@ class _HomeScreenState extends State<HomeScreen> {
if (myScore < oppScore) result = 'D'; if (myScore < oppScore) result = 'D';
return _buildGameHistoryCard( return _buildGameHistoryCard(
context: context, context: context, // Usamos o context para o sf
opponent: opponent, opponent: opponent,
result: result, result: result,
myScore: myScore, myScore: myScore,
@@ -248,14 +247,13 @@ 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 * safeSf), SizedBox(height: 20 * context.sf),
], ],
), ),
), ),
@@ -325,14 +323,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 double safeSf required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp
}) { }) {
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 * safeSf), margin: EdgeInsets.only(bottom: 14 * context.sf),
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))],
@@ -340,34 +338,34 @@ class _HomeScreenState extends State<HomeScreen> {
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: EdgeInsets.all(14 * safeSf), padding: EdgeInsets.all(14 * context.sf),
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 36 * safeSf, height: 36 * safeSf, width: 36 * context.sf, height: 36 * context.sf,
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 * safeSf))), child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf))),
), ),
SizedBox(width: 14 * safeSf), SizedBox(width: 14 * context.sf),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(date, style: TextStyle(fontSize: 11 * safeSf, color: Colors.grey, fontWeight: FontWeight.w600)), Text(date, style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.w600)),
SizedBox(height: 6 * safeSf), SizedBox(height: 6 * context.sf),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * safeSf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)), Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 8 * safeSf), padding: EdgeInsets.symmetric(horizontal: 8 * context.sf),
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 8 * safeSf, vertical: 4 * safeSf), padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
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 * safeSf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)), child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)),
), ),
), ),
Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * safeSf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)), Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)),
], ],
), ),
], ],
@@ -378,27 +376,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 * safeSf, vertical: 12 * safeSf), width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf),
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(Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, safeSf, isMvp: true)), Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)),
Expanded(child: _buildGridStatRow(Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef, safeSf)), Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef)),
], ],
), ),
SizedBox(height: 8 * safeSf), SizedBox(height: 8 * context.sf),
Row( Row(
children: [ children: [
Expanded(child: _buildGridStatRow(Icons.bolt, Colors.blue.shade700, "Pontos", topPts, safeSf)), Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)),
Expanded(child: _buildGridStatRow(Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs, safeSf)), Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs)),
], ],
), ),
SizedBox(height: 8 * safeSf), SizedBox(height: 8 * context.sf),
Row( Row(
children: [ children: [
Expanded(child: _buildGridStatRow(Icons.star, Colors.green.shade700, "Assists", topAst, safeSf)), Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)),
const Expanded(child: SizedBox()), const Expanded(child: SizedBox()),
], ],
), ),
@@ -410,17 +408,17 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
Widget _buildGridStatRow(IconData icon, Color color, String label, String value, double safeSf, {bool isMvp = false}) { Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, String label, String value, {bool isMvp = false}) {
return Row( return Row(
children: [ children: [
Icon(icon, size: 14 * safeSf, color: color), Icon(icon, size: 14 * context.sf, color: color),
SizedBox(width: 4 * safeSf), SizedBox(width: 4 * context.sf),
Text('$label: ', style: TextStyle(fontSize: 11 * safeSf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
Expanded( Expanded(
child: Text( child: Text(
value, value,
style: TextStyle( style: TextStyle(
fontSize: 11 * safeSf, fontSize: 11 * context.sf,
color: isMvp ? Colors.amber.shade900 : Colors.black87, color: isMvp ? Colors.amber.shade900 : Colors.black87,
fontWeight: FontWeight.bold fontWeight: FontWeight.bold
), ),

View File

@@ -1,10 +1,7 @@
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'; import '../utils/size_extension.dart'; // 👇 A MAGIA DO SF!
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});
@@ -22,70 +19,19 @@ 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 * safeSf), padding: EdgeInsets.all(16.0 * context.sf),
child: InkWell( child: InkWell(
onTap: () => _showTeamSelector(context, safeSf), onTap: () => _showTeamSelector(context),
child: Container( child: Container(
padding: EdgeInsets.all(12 * safeSf), padding: EdgeInsets.all(12 * context.sf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(15 * safeSf), borderRadius: BorderRadius.circular(15 * context.sf),
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)]
), ),
@@ -93,9 +39,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 * safeSf), Icon(Icons.shield, color: const Color(0xFFE74C3C), size: 24 * context.sf),
SizedBox(width: 10 * safeSf), SizedBox(width: 10 * context.sf),
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * safeSf, fontWeight: FontWeight.bold)) Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold))
]), ]),
const Icon(Icons.arrow_drop_down), const Icon(Icons.arrow_drop_down),
], ],
@@ -104,10 +50,9 @@ 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 * safeSf))) ? Center(child: Text("Seleciona uma equipa acima.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)))
: 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) {
@@ -122,7 +67,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 * safeSf))); if (membersData.isEmpty) return Center(child: Text("Esta equipa não tem jogadores registados.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)));
final statsData = statsSnapshot.data ?? []; final statsData = statsSnapshot.data ?? [];
final gamesData = gamesSnapshot.data ?? []; final gamesData = gamesSnapshot.data ?? [];
@@ -137,7 +82,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, safeSf, screenWidth); return _buildStatsGrid(context, playerTotals, teamTotals);
} }
); );
} }
@@ -149,21 +94,29 @@ 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]!['rbs'] += (row['rbs'] ?? 0); aggregated[name]!['stl'] += (row['stl'] ?? 0); aggregated[name]!['blk'] += (row['blk'] ?? 0); aggregated[name]!['j'] += 1;
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? defRaw = game['top_def_name']; String? mvp = game['mvp_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();
@@ -181,113 +134,92 @@ 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, double safeSf, double screenWidth) { Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals) {
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: ConstrainedBox( child: DataTable(
constraints: BoxConstraints(minWidth: screenWidth), columnSpacing: 25 * context.sf,
child: DataTable( headingRowColor: MaterialStateProperty.all(Colors.grey.shade100),
columnSpacing: dynamicSpacing, dataRowHeight: 60 * context.sf,
horizontalMargin: 20 * safeSf, columns: [
headingRowColor: MaterialStateProperty.all(Colors.grey.shade100), DataColumn(label: const Text('JOGADOR')),
dataRowHeight: 60 * safeSf, _buildSortableColumn(context, 'J', 'j'),
columns: [ _buildSortableColumn(context, 'PTS', 'pts'),
DataColumn(label: const Text('JOGADOR')), _buildSortableColumn(context, 'AST', 'ast'),
_buildSortableColumn('J', 'j', safeSf), _buildSortableColumn(context, 'RBS', 'rbs'),
_buildSortableColumn('PTS', 'pts', safeSf), _buildSortableColumn(context, 'STL', 'stl'),
_buildSortableColumn('AST', 'ast', safeSf), _buildSortableColumn(context, 'BLK', 'blk'),
_buildSortableColumn('RBS', 'rbs', safeSf), _buildSortableColumn(context, 'DEF 🛡️', 'def'),
_buildSortableColumn('STL', 'stl', safeSf), _buildSortableColumn(context, 'MVP 🏆', 'mvp'),
_buildSortableColumn('BLK', 'blk', safeSf), ],
_buildSortableColumn('DEF 🛡️', 'def', safeSf), rows: [
_buildSortableColumn('MVP 🏆', 'mvp', safeSf), ...players.map((player) => DataRow(cells: [
], 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))])),
rows: [ DataCell(Center(child: Text(player['j'].toString()))),
...players.map((player) => DataRow(cells: [ _buildStatCell(context, player['pts'], isHighlight: true),
DataCell( _buildStatCell(context, player['ast']),
// 👇 TORNEI O NOME CLICÁVEL PARA ABRIR O MAPA _buildStatCell(context, player['rbs']),
InkWell( _buildStatCell(context, player['stl']),
onTap: () => _openPlayerHeatmap(player['name']), _buildStatCell(context, player['blk']),
child: Row(children: [ _buildStatCell(context, player['def'], isBlue: true),
CircleAvatar(radius: 15 * safeSf, backgroundColor: Colors.grey.shade200, child: Icon(Icons.person, size: 18 * safeSf)), _buildStatCell(context, player['mvp'], isGold: true),
SizedBox(width: 10 * safeSf), ])),
Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * safeSf, color: Colors.blue.shade700)) DataRow(
]), 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(player['j'].toString()))), DataCell(Center(child: Text(teamTotals['j'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)))),
_buildStatCell(player['pts'], safeSf, isHighlight: true), _buildStatCell(context, teamTotals['pts'], isHighlight: true),
_buildStatCell(player['ast'], safeSf), _buildStatCell(context, teamTotals['ast']),
_buildStatCell(player['rbs'], safeSf), _buildStatCell(context, teamTotals['rbs']),
_buildStatCell(player['stl'], safeSf), _buildStatCell(context, teamTotals['stl']),
_buildStatCell(player['blk'], safeSf), _buildStatCell(context, teamTotals['blk']),
_buildStatCell(player['def'], safeSf, isBlue: true), _buildStatCell(context, teamTotals['def'], isBlue: true),
_buildStatCell(player['mvp'], safeSf, isGold: true), _buildStatCell(context, teamTotals['mvp'], 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),
]
)
],
),
), ),
), ),
), ),
); );
} }
// (Outras funções de build continuam igual...) DataColumn _buildSortableColumn(BuildContext context, String title, String sortKey) {
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( child: Row(children: [
mainAxisSize: MainAxisSize.min, Text(title, style: TextStyle(fontSize: 12 * context.sf, fontWeight: FontWeight.bold)),
children: [ if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * context.sf, color: const Color(0xFFE74C3C)),
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(int value, double safeSf, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) { DataCell _buildStatCell(BuildContext context, int value, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) {
return DataCell(Center(child: Container( return DataCell(Center(child: Container(
padding: EdgeInsets.symmetric(horizontal: 8 * safeSf, vertical: 4 * safeSf), padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
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 * safeSf, color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? Colors.green.shade700 : Colors.black87)) fontSize: 14 * context.sf, color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? Colors.green.shade700 : Colors.black87))
)), )),
))); )));
} }
void _showTeamSelector(BuildContext context, double safeSf) { void _showTeamSelector(BuildContext context) {
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'], style: TextStyle(fontSize: 15 * safeSf)), title: Text(teams[i]['name']),
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,8 +2,7 @@ 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'; import '../utils/size_extension.dart'; // 👇 IMPORTANTE: O TEU NOVO SUPERPODER
import 'dart:math' as math;
class TeamsPage extends StatefulWidget { class TeamsPage extends StatefulWidget {
const TeamsPage({super.key}); const TeamsPage({super.key});
@@ -122,6 +121,7 @@ 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', heroTag: 'add_team_btn', // 👇 A MÁGICA ESTÁ AQUI!
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,33 +151,30 @@ 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 * safeSf), padding: EdgeInsets.all(16.0 * context.sf),
child: TextField( child: TextField(
controller: _searchController, controller: _searchController,
onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()), onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()),
style: TextStyle(fontSize: 16 * safeSf), style: TextStyle(fontSize: 16 * context.sf),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Pesquisar equipa...', hintText: 'Pesquisar equipa...',
hintStyle: TextStyle(fontSize: 16 * safeSf), hintStyle: TextStyle(fontSize: 16 * context.sf),
prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * safeSf), prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * context.sf),
filled: true, filled: true,
fillColor: Colors.white, fillColor: Colors.white,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * safeSf), borderSide: BorderSide.none), border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * context.sf), 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 * safeSf))); if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * context.sf)));
var data = List<Map<String, dynamic>>.from(snapshot.data!); var data = List<Map<String, dynamic>>.from(snapshot.data!);
@@ -194,7 +191,7 @@ class _TeamsPageState extends State<TeamsPage> {
}); });
return ListView.builder( return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16 * safeSf), // Margem perfeitamente alinhada padding: EdgeInsets.symmetric(horizontal: 16 * context.sf),
itemCount: data.length, itemCount: data.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final team = Team.fromMap(data[index]); final team = Team.fromMap(data[index]);
@@ -227,70 +224,68 @@ 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 * safeSf), color: Colors.white, elevation: 3, margin: EdgeInsets.only(bottom: 12 * context.sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * safeSf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
child: ListTile( child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 8 * safeSf), contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 8 * context.sf),
leading: Stack( leading: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 28 * safeSf, backgroundColor: Colors.grey[200], radius: 28 * context.sf, 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 * safeSf)) : null, child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http')) ? Text(team.imageUrl.isEmpty ? "🏀" : team.imageUrl, style: TextStyle(fontSize: 24 * context.sf)) : null,
), ),
Positioned( Positioned(
left: -15 * safeSf, top: -10 * safeSf, left: -15 * context.sf, top: -10 * context.sf,
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 * safeSf, shadows: [Shadow(color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), blurRadius: 4 * safeSf)]), 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)]),
onPressed: onFavoriteTap, onPressed: onFavoriteTap,
), ),
), ),
], ],
), ),
title: Text(team.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * safeSf), overflow: TextOverflow.ellipsis), title: Text(team.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * context.sf), overflow: TextOverflow.ellipsis),
subtitle: Padding( subtitle: Padding(
padding: EdgeInsets.only(top: 6.0 * safeSf), padding: EdgeInsets.only(top: 6.0 * context.sf),
child: Row( child: Row(
children: [ children: [
Icon(Icons.groups_outlined, size: 16 * safeSf, color: Colors.grey), Icon(Icons.groups_outlined, size: 16 * context.sf, color: Colors.grey),
SizedBox(width: 4 * safeSf), SizedBox(width: 4 * context.sf),
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 * safeSf)); return Text("$count Jogs.", style: TextStyle(color: count > 0 ? Colors.green[700] : Colors.orange, fontWeight: FontWeight.bold, fontSize: 13 * context.sf));
}, },
), ),
SizedBox(width: 8 * safeSf), SizedBox(width: 8 * context.sf),
Expanded(child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * safeSf), overflow: TextOverflow.ellipsis)), Expanded(child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * context.sf), 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 * safeSf), 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 * context.sf), 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 * safeSf), onPressed: () => _confirmDelete(context, safeSf)), IconButton(tooltip: 'Eliminar Equipa', icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * context.sf), onPressed: () => _confirmDelete(context)),
], ],
), ),
), ),
); );
} }
void _confirmDelete(BuildContext context, double safeSf) { void _confirmDelete(BuildContext context) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)), title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * safeSf)), content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * context.sf)),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * safeSf))), TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))),
TextButton(onPressed: () { controller.deleteTeam(team.id); Navigator.pop(context); }, child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * safeSf))), TextButton(onPressed: () { controller.deleteTeam(team.id); Navigator.pop(context); }, child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * context.sf))),
], ],
), ),
); );
@@ -313,37 +308,32 @@ 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 * safeSf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)), title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Container( child: Column(
constraints: BoxConstraints(maxWidth: 450 * safeSf), // O popup pode ter um travão para não cobrir a tela toda, fica mais bonito mainAxisSize: MainAxisSize.min,
child: Column( children: [
mainAxisSize: MainAxisSize.min, TextField(controller: _nameController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * context.sf)), textCapitalization: TextCapitalization.words),
children: [ SizedBox(height: 15 * context.sf),
TextField(controller: _nameController, style: TextStyle(fontSize: 14 * safeSf), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * safeSf)), textCapitalization: TextCapitalization.words), DropdownButtonFormField<String>(
SizedBox(height: 15 * safeSf), value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf)),
DropdownButtonFormField<String>( style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87),
value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * safeSf)), items: ['2023/24', '2024/25', '2025/26'].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
style: TextStyle(fontSize: 14 * safeSf, color: Colors.black87), onChanged: (val) => setState(() => _selectedSeason = val!),
items: ['2023/24', '2024/25', '2025/26'].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), ),
onChanged: (val) => setState(() => _selectedSeason = val!), SizedBox(height: 15 * context.sf),
), 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 * safeSf))), TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), padding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 10 * safeSf)), style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)),
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 * safeSf)), child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * context.sf)),
), ),
], ],
); );

View File

@@ -1,21 +1,18 @@
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'; import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
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 * safeSf, width: 200 * context.sf, // Ajusta o tamanho da imagem suavemente
height: 200 * safeSf, height: 200 * context.sf,
child: Image.asset( child: Image.asset(
'assets/playmaker-logos.png', 'assets/playmaker-logos.png',
fit: BoxFit.contain, fit: BoxFit.contain,
@@ -24,16 +21,16 @@ class BasketTrackHeader extends StatelessWidget {
Text( Text(
'BasketTrack', 'BasketTrack',
style: TextStyle( style: TextStyle(
fontSize: 36 * safeSf, fontSize: 36 * context.sf,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.grey[900], color: Colors.grey[900],
), ),
), ),
SizedBox(height: 6 * safeSf), SizedBox(height: 6 * context.sf),
Text( Text(
'Gere as tuas equipas e estatísticas', 'Gere as tuas equipas e estatísticas',
style: TextStyle( style: TextStyle(
fontSize: 16 * safeSf, fontSize: 16 * context.sf,
color: Colors.grey[600], color: Colors.grey[600],
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
@@ -51,42 +48,40 @@ 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 * safeSf), style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'E-mail', labelText: 'E-mail',
labelStyle: TextStyle(fontSize: 15 * safeSf), labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.email_outlined, size: 22 * safeSf), prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf),
errorText: controller.emailError, errorText: controller.emailError,
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, keyboardType: TextInputType.emailAddress,
), ),
SizedBox(height: 20 * safeSf), SizedBox(height: 20 * context.sf),
TextField( TextField(
controller: controller.passwordController, controller: controller.passwordController,
obscureText: controller.obscurePassword, obscureText: controller.obscurePassword,
style: TextStyle(fontSize: 15 * safeSf), style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Palavra-passe', labelText: 'Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * safeSf), labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * safeSf), prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf),
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 * safeSf size: 22 * context.sf
), ),
onPressed: controller.togglePasswordVisibility, onPressed: controller.togglePasswordVisibility,
), ),
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),
), ),
), ),
], ],
@@ -102,11 +97,9 @@ 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 * safeSf, height: 58 * context.sf,
child: ElevatedButton( child: ElevatedButton(
onPressed: controller.isLoading ? null : () async { onPressed: controller.isLoading ? null : () async {
final success = await controller.login(); final success = await controller.login();
@@ -115,15 +108,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 * safeSf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
elevation: 3, elevation: 3,
), ),
child: controller.isLoading child: controller.isLoading
? SizedBox( ? SizedBox(
width: 28 * safeSf, height: 28 * safeSf, width: 28 * context.sf, height: 28 * context.sf,
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 * safeSf, fontWeight: FontWeight.bold)), : Text('Entrar', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
), ),
); );
} }
@@ -134,21 +127,19 @@ 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 * safeSf, height: 58 * context.sf,
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 * safeSf), side: BorderSide(color: const Color(0xFFE74C3C), width: 2 * context.sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * safeSf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
), ),
child: Text('Criar Conta', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)), child: Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
), ),
); );
} }

View File

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

View File

@@ -2,13 +2,12 @@ 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 original final double sf; // <-- Variável de escala
const TeamCard({ const TeamCard({
super.key, super.key,
@@ -20,24 +19,20 @@ 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 * safeSf), margin: EdgeInsets.only(bottom: 12 * sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * safeSf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)),
child: ListTile( child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 8 * safeSf), contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf),
// --- 1. IMAGEM + FAVORITO --- // --- 1. IMAGEM + FAVORITO ---
leading: Stack( leading: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 28 * safeSf, radius: 28 * sf,
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)
@@ -45,22 +40,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 * safeSf), style: TextStyle(fontSize: 24 * sf),
) )
: null, : null,
), ),
Positioned( Positioned(
left: -15 * safeSf, left: -15 * sf,
top: -10 * safeSf, top: -10 * sf,
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 * safeSf, size: 28 * sf,
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 * safeSf, blurRadius: 4 * sf,
), ),
], ],
), ),
@@ -73,39 +68,40 @@ 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 * safeSf), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos
), ),
// --- 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 * safeSf), padding: EdgeInsets.only(top: 6.0 * sf),
child: Row( child: Row(
children: [ children: [
Icon(Icons.groups_outlined, size: 16 * safeSf, color: Colors.grey), Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
SizedBox(width: 4 * safeSf), SizedBox(width: 4 * sf),
// 👇 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.", "$count Jogs.", // Abreviado para poupar espaço
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 * safeSf, fontSize: 13 * sf,
), ),
); );
}, },
), ),
SizedBox(width: 8 * safeSf), SizedBox(width: 8 * sf),
Expanded( Expanded( // Garante que a temporada se adapta se faltar espaço
child: Text( child: Text(
"| ${team.season}", "| ${team.season}",
style: TextStyle(color: Colors.grey, fontSize: 13 * safeSf), style: TextStyle(color: Colors.grey, fontSize: 13 * sf),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@@ -115,11 +111,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, mainAxisSize: MainAxisSize.min, // <-- ISTO RESOLVE O OVERFLOW DAS RISCAS AMARELAS
children: [ children: [
IconButton( IconButton(
tooltip: 'Ver Estatísticas', tooltip: 'Ver Estatísticas',
icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * safeSf), icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * sf),
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
@@ -131,8 +127,8 @@ class TeamCard extends StatelessWidget {
), ),
IconButton( IconButton(
tooltip: 'Eliminar Equipa', tooltip: 'Eliminar Equipa',
icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * safeSf), icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * sf),
onPressed: () => _confirmDelete(context, safeSf), onPressed: () => _confirmDelete(context),
), ),
], ],
), ),
@@ -140,23 +136,23 @@ class TeamCard extends StatelessWidget {
); );
} }
void _confirmDelete(BuildContext context, double safeSf) { void _confirmDelete(BuildContext context) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)), title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold)),
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * safeSf)), content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * sf)),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * safeSf)), child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf)),
), ),
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 * safeSf)), child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * sf)),
), ),
], ],
), ),
@@ -167,7 +163,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; final double sf; // Recebe a escala
const CreateTeamDialog({super.key, required this.onConfirm, required this.sf}); const CreateTeamDialog({super.key, required this.onConfirm, required this.sf});
@@ -182,65 +178,58 @@ 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 * safeSf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)), title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold)),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Container( child: Column(
// 👇 Limita a largura máxima no tablet para o popup não ficar super esticado! mainAxisSize: MainAxisSize.min,
constraints: BoxConstraints(maxWidth: 450 * safeSf), children: [
child: Column( TextField(
mainAxisSize: MainAxisSize.min, controller: _nameController,
children: [ style: TextStyle(fontSize: 14 * widget.sf),
TextField( decoration: InputDecoration(
controller: _nameController, labelText: 'Nome da Equipa',
style: TextStyle(fontSize: 14 * safeSf), labelStyle: TextStyle(fontSize: 14 * widget.sf)
decoration: InputDecoration(
labelText: 'Nome da Equipa',
labelStyle: TextStyle(fontSize: 14 * safeSf)
),
textCapitalization: TextCapitalization.words,
), ),
SizedBox(height: 15 * safeSf), textCapitalization: TextCapitalization.words,
DropdownButtonFormField<String>( ),
value: _selectedSeason, SizedBox(height: 15 * widget.sf),
decoration: InputDecoration( DropdownButtonFormField<String>(
labelText: 'Temporada', value: _selectedSeason,
labelStyle: TextStyle(fontSize: 14 * safeSf) decoration: InputDecoration(
), labelText: 'Temporada',
style: TextStyle(fontSize: 14 * safeSf, color: Colors.black87), labelStyle: TextStyle(fontSize: 14 * widget.sf)
items: ['2023/24', '2024/25', '2025/26']
.map((s) => DropdownMenuItem(value: s, child: Text(s)))
.toList(),
onChanged: (val) => setState(() => _selectedSeason = val!),
), ),
SizedBox(height: 15 * safeSf), style: TextStyle(fontSize: 14 * widget.sf, color: Colors.black87),
TextField( items: ['2023/24', '2024/25', '2025/26']
controller: _imageController, .map((s) => DropdownMenuItem(value: s, child: Text(s)))
style: TextStyle(fontSize: 14 * safeSf), .toList(),
decoration: InputDecoration( onChanged: (val) => setState(() => _selectedSeason = val!),
labelText: 'URL Imagem ou Emoji', ),
labelStyle: TextStyle(fontSize: 14 * safeSf), SizedBox(height: 15 * widget.sf),
hintText: 'Ex: 🏀 ou https://...', TextField(
hintStyle: TextStyle(fontSize: 14 * safeSf) controller: _imageController,
), 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 * safeSf)) child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf))
), ),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C), backgroundColor: const Color(0xFFE74C3C),
padding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 10 * safeSf) padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)
), ),
onPressed: () { onPressed: () {
if (_nameController.text.trim().isNotEmpty) { if (_nameController.text.trim().isNotEmpty) {
@@ -252,7 +241,7 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
Navigator.pop(context); Navigator.pop(context);
} }
}, },
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * safeSf)), child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)),
), ),
], ],
); );

View File

@@ -61,10 +61,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.1"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -268,18 +268,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.17" version: "0.12.18"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.13.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -553,10 +553,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.7" version: "0.7.9"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description: