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

@@ -32,6 +32,7 @@ class PlacarController {
bool isLoading = true;
bool isSaving = false;
bool gameWasAlreadyFinished = false;
int myScore = 0;
@@ -66,27 +67,35 @@ class PlacarController {
Timer? timer;
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;
double hoopBaseX = 0.000;
double arcRadius = 0.500;
double cornerY = 0.443;
double hoopBaseX = 0.088;
double arcRadius = 0.459;
double cornerY = 0.440;
Future<void> loadPlayers() async {
final supabase = Supabase.instance.client;
try {
await Future.delayed(const Duration(milliseconds: 1500));
myCourt.clear(); myBench.clear(); oppCourt.clear(); oppBench.clear();
playerStats.clear(); playerNumbers.clear(); playerDbIds.clear();
myFouls = 0; opponentFouls = 0;
myCourt.clear();
myBench.clear();
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();
myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0;
opponentScore = int.tryParse(gameResponse['opponent_score']?.toString() ?? '0') ?? 0;
int totalSeconds = int.tryParse(gameResponse['remaining_seconds']?.toString() ?? '600') ?? 600;
duration = Duration(seconds: totalSeconds);
myTimeoutsUsed = int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0;
opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
currentQuarter = int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1;
@@ -110,10 +119,17 @@ class PlacarController {
for (int i = 0; i < myPlayers.length; i++) {
String dbId = myPlayers[i]['id'].toString();
String name = myPlayers[i]['name'].toString();
_registerPlayer(name: name, number: myPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: true, isCourt: i < 5);
if (savedStats.containsKey(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);
}
}
@@ -122,28 +138,28 @@ class PlacarController {
for (int i = 0; i < oppPlayers.length; i++) {
String dbId = oppPlayers[i]['id'].toString();
String name = oppPlayers[i]['name'].toString();
_registerPlayer(name: name, number: oppPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: false, isCourt: i < 5);
if (savedStats.containsKey(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);
}
}
_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;
onUpdate();
} catch (e) {
debugPrint("Erro ao retomar jogo: $e");
_padTeam(myCourt, myBench, "Falha", isMyTeam: true);
_padTeam(oppCourt, oppBench, "Falha Opp", isMyTeam: false);
isLoading = false;
onUpdate();
}
@@ -153,9 +169,17 @@ class PlacarController {
if (playerNumbers.containsKey(name)) name = "$name (Opp)";
playerNumbers[name] = number;
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); }
else { if (isCourt) oppCourt.add(name); else oppBench.add(name); }
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);
} else {
if (isCourt) oppCourt.add(name); else oppBench.add(name);
}
}
void _padTeam(List<String> court, List<String> bench, String prefix, {required bool isMyTeam}) {
@@ -177,8 +201,13 @@ class PlacarController {
if (currentQuarter < 4) {
currentQuarter++;
duration = const Duration(minutes: 10);
myFouls = 0; opponentFouls = 0; myTimeoutsUsed = 0; opponentTimeoutsUsed = 0;
onUpdate();
myFouls = 0;
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();
@@ -189,8 +218,11 @@ class PlacarController {
}
void useTimeout(bool isOpponent) {
if (isOpponent) { if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++; }
else { if (myTimeoutsUsed < 3) myTimeoutsUsed++; }
if (isOpponent) {
if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++;
} else {
if (myTimeoutsUsed < 3) myTimeoutsUsed++;
}
isRunning = false;
timer?.cancel();
onUpdate();
@@ -226,6 +258,7 @@ class PlacarController {
myCourt[courtIndex] = benchPlayer;
myBench[benchIndex] = courtPlayerName;
showMyBench = false;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer')));
}
if (action.startsWith("bench_opp_") && isOpponent) {
String benchPlayer = action.replaceAll("bench_opp_", "");
@@ -235,36 +268,46 @@ class PlacarController {
oppCourt[courtIndex] = benchPlayer;
oppBench[benchIndex] = courtPlayerName;
showOppBench = false;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer')));
}
onUpdate();
}
// ==============================================================
// 🎯 REGISTO DO TOQUE (INTELIGENTE E SILENCIOSO)
// ==============================================================
// =========================================================================
// 👇 A MÁGICA DOS PONTOS ACONTECE AQUI 👇
// =========================================================================
void registerShotLocation(BuildContext context, Offset position, Size size) {
if (pendingAction == null || pendingPlayer == null) return;
bool isOpponent = pendingPlayer!.startsWith("player_opp_");
bool is3Pt = pendingAction!.contains("_3");
bool is2Pt = pendingAction!.contains("_2");
// O ÁRBITRO MATEMÁTICO COM AS TUAS VARIÁVEIS CALIBRADAS
if (is3Pt || is2Pt) {
bool isInside2Pts = _validateShotZone(position, size, isOpponent);
bool isValid = _validateShotZone(position, size, is3Pt);
// Bloqueio silencioso (sem notificações chamas)
if ((is2Pt && !isInside2Pts) || (is3Pt && isInside2Pts)) {
cancelShotLocation();
return;
// SE A JOGADA FOI NO SÍTIO ERRADO
if (!isValid) {
return; // <-- ESTE RETURN BLOQUEIA A GRAVAÇÃO DO PONTO!
}
}
// SE A JOGADA FOI VÁLIDA:
bool isMake = pendingAction!.startsWith("add_pts_");
double relX = position.dx / size.width;
double relY = position.dy / size.height;
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!);
isSelectingShotLocation = false;
@@ -273,37 +316,36 @@ class PlacarController {
onUpdate();
}
// ==============================================================
// 📐 MATEMÁTICA PURA: LÓGICA DE MEIO-CAMPO ATACANTE (SOLUÇÃO DIVIDIDA)
// ==============================================================
bool _validateShotZone(Offset position, Size size, bool isOpponent) {
bool _validateShotZone(Offset position, Size size, bool is3Pt) {
double relX = position.dx / size.width;
double relY = position.dy / size.height;
double hX = hoopBaseX;
double radius = arcRadius;
double cY = cornerY;
// A Minha Equipa defende na Esquerda (0.0), logo ataca o cesto da Direita (1.0)
// O Adversário defende na Direita (1.0), logo ataca o cesto da Esquerda (0.0)
double hoopX = isOpponent ? hX : (1.0 - hX);
bool isLeftHalf = relX < 0.5;
double hoopX = isLeftHalf ? hoopBaseX : (1.0 - hoopBaseX);
double hoopY = 0.50;
double aspectRatio = size.width / size.height;
double distFromCenterY = (relY - hoopY).abs();
// Descobre se o toque foi feito na metade atacante daquela equipa
bool isAttackingHalf = isOpponent ? (relX < 0.5) : (relX > 0.5);
bool isInside2Pts;
if (isAttackingHalf && distFromCenterY > cY) {
return false; // É 3 pontos (Zona dos Cantos)
} else {
// Lógica das laterais (Cantos)
if (distFromCenterY > cornerY) {
double distToBaseline = isLeftHalf ? relX : (1.0 - relX);
isInside2Pts = distToBaseline <= hoopBaseX;
}
// Lógica da Curva Frontal
else {
double dx = (relX - hoopX) * aspectRatio;
double dy = (relY - hoopY);
double distanceToHoop = math.sqrt((dx * dx) + (dy * dy));
return distanceToHoop <= radius;
isInside2Pts = distanceToHoop < arcRadius;
}
if (is3Pt) return !isInside2Pts;
return isInside2Pts;
}
// 👆 ===================================================================== 👆
void cancelShotLocation() {
isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate();
@@ -359,7 +401,7 @@ class PlacarController {
onUpdate();
try {
bool isGameFinishedNow = (currentQuarter >= 4 && duration.inSeconds == 0);
bool isGameFinishedNow = currentQuarter >= 4 && duration.inSeconds == 0;
String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado';
String topPtsName = '---'; int maxPts = -1;
@@ -374,8 +416,10 @@ class PlacarController {
int rbs = stats['rbs'] ?? 0;
int stl = stats['stl'] ?? 0;
int blk = stats['blk'] ?? 0;
int defScore = stl + blk;
int mvpScore = pts + ast + rbs + defScore;
if (pts > maxPts && pts > 0) { maxPts = pts; topPtsName = '$playerName ($pts)'; }
if (ast > maxAst && ast > 0) { maxAst = ast; topAstName = '$playerName ($ast)'; }
if (rbs > maxRbs && rbs > 0) { maxRbs = rbs; topRbsName = '$playerName ($rbs)'; }
@@ -384,32 +428,54 @@ class PlacarController {
});
await supabase.from('games').update({
'my_score': myScore, 'opponent_score': opponentScore, 'remaining_seconds': duration.inSeconds,
'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,
'my_score': myScore,
'opponent_score': opponentScore,
'remaining_seconds': duration.inSeconds,
'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);
// Atualiza Vitórias/Derrotas se o jogo terminou
if (isGameFinishedNow && !gameWasAlreadyFinished && myTeamDbId != null && oppTeamDbId != null) {
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) {
if(t['id'].toString() == myTeamDbId) {
int w = (t['wins'] ?? 0) + (myScore > opponentScore ? 1 : 0);
int l = (t['losses'] ?? 0) + (myScore < opponentScore ? 1 : 0);
int d = (t['draws'] ?? 0) + (myScore == opponentScore ? 1 : 0);
await supabase.from('teams').update({'wins': w, 'losses': l, 'draws': d}).eq('id', myTeamDbId!);
if(t['id'].toString() == myTeamDbId) myTeamUpdate = Map.from(t);
if(t['id'].toString() == oppTeamDbId) oppTeamUpdate = Map.from(t);
}
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 {
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!);
}
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;
}
// Salvar Estatísticas Gerais
List<Map<String, dynamic>> batchStats = [];
playerStats.forEach((playerName, stats) {
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);
// ===============================================
// 🔥 GRAVAR COORDENADAS PARA O HEATMAP
// ===============================================
List<Map<String, dynamic>> shotsData = [];
for (var shot in matchShots) {
bool isMyTeamPlayer = myCourt.contains(shot.playerName) || myBench.contains(shot.playerName);
shotsData.add({
'game_id': gameId,
'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!,
'player_name': shot.playerName,
'relative_x': shot.relativeX,
'relative_y': shot.relativeY,
'is_make': shot.isMake,
});
await supabase.from('player_stats').delete().eq('game_id', gameId);
if (batchStats.isNotEmpty) {
await supabase.from('player_stats').insert(batchStats);
}
await supabase.from('game_shots').delete().eq('game_id', gameId);
if (shotsData.isNotEmpty) await supabase.from('game_shots').insert(shotsData);
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) {
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 {
isSaving = false;
onUpdate();

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart';
import 'dados_grafico.dart';
import 'dart:math' as math;
class PieChartCard extends StatefulWidget {
final int victories;
@@ -64,21 +63,26 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
animation: _animation,
builder: (context, child) {
return Transform.scale(
// O scale pode passar de 1.0 (efeito back), mas a opacidade NÃO
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(
margin: EdgeInsets.zero,
elevation: 8,
shadowColor: Colors.black54,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
child: InkWell(
onTap: widget.onTap,
borderRadius: BorderRadius.circular(14),
child: Container(
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(
builder: (context, constraints) {
@@ -86,31 +90,29 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
final double cw = constraints.maxWidth;
return Padding(
padding: EdgeInsets.symmetric(horizontal: cw * 0.05, vertical: ch * 0.03),
padding: EdgeInsets.all(cw * 0.06),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- CABEÇALHO ---
// 👇 TÍTULOS UM POUCO MAIS PRESENTES
FittedBox(
fit: BoxFit.scaleDown,
child: Text(widget.title.toUpperCase(),
style: TextStyle(fontSize: ch * 0.045, fontWeight: FontWeight.bold, color: Colors.white70, letterSpacing: 1.2)),
child: Text(widget.title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), letterSpacing: 1.0)),
),
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(
flex: 9,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Lado Esquerdo: Donut Chart LIMPO (Sem texto sobreposto)
SizedBox(
width: cw * 0.38,
height: cw * 0.38,
Expanded(
flex: 1,
child: PieChartWidget(
victoryPercentage: data.victoryPercentage,
defeatPercentage: data.defeatPercentage,
@@ -118,60 +120,59 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
sf: widget.sf,
),
),
SizedBox(width: cw * 0.08),
// 2. Lado Direito: Números Dinâmicos
SizedBox(width: cw * 0.05),
Expanded(
child: FittedBox(
alignment: Alignment.centerLeft,
fit: BoxFit.scaleDown,
flex: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.greenAccent, ch, cw),
_buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.yellowAccent, ch, cw),
_buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.redAccent, ch, cw),
_buildDynDivider(cw),
_buildDynStatRow("TOT", data.total.toString(), "100", Colors.white, ch, cw),
_buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, ch),
_buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.yellow, ch),
_buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, ch),
_buildDynDivider(ch),
_buildDynStatRow("TOT", data.total.toString(), "100", Colors.white, ch),
],
),
),
),
],
),
),
const Expanded(flex: 1, child: SizedBox()),
// --- RODAPÉ: BOTÃO WIN RATE GIGANTE ---
// 👇 RODAPÉ AJUSTADO
SizedBox(height: ch * 0.03),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: ch * 0.025),
padding: EdgeInsets.symmetric(vertical: ch * 0.035),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
color: Colors.white24, // Igual ao fundo do botão detalhes
borderRadius: BorderRadius.circular(ch * 0.03), // Borda arredondada
),
child: Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.stars, color: Colors.greenAccent, size: ch * 0.075),
const SizedBox(width: 10),
Text('WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
Icon(
data.victoryPercentage >= 0.5 ? Icons.trending_up : Icons.trending_down,
color: Colors.green,
size: ch * 0.09
),
SizedBox(width: cw * 0.02),
Text(
'WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
fontSize: ch * 0.06
),
fontSize: ch * 0.05,
fontWeight: FontWeight.bold,
color: Colors.white
)
),
],
),
),
),
),
],
),
);
@@ -182,38 +183,34 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
),
);
}
Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch, double cw) {
// 👇 PERCENTAGENS SUBIDAS LIGEIRAMENTE (0.10 e 0.045)
Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch) {
return Padding(
padding: EdgeInsets.symmetric(vertical: ch * 0.005),
padding: EdgeInsets.only(bottom: ch * 0.01),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: cw * 0.12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(label, style: TextStyle(fontSize: ch * 0.035, color: Colors.white60, fontWeight: FontWeight.bold)),
Text('$percent%', style: TextStyle(fontSize: ch * 0.04, color: color, fontWeight: FontWeight.bold)),
],
// Número subiu para 0.10
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)))),
SizedBox(width: ch * 0.02),
Expanded(
flex: 3,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Row(children: [
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) {
return Container(
width: cw * 0.35,
height: 1.5,
color: Colors.white24,
margin: const EdgeInsets.symmetric(vertical: 4)
);
Widget _buildDynDivider(double ch) {
return Container(height: 0.5, color: Colors.white.withOpacity(0.1), margin: EdgeInsets.symmetric(vertical: ch * 0.01));
}
}

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.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/widgets/placar_widgets.dart';
import 'dart:math' as math;
@@ -43,15 +42,29 @@ class _PlacarPageState extends State<PlacarPage> {
super.dispose();
}
// --- BOTÕES FLUTUANTES DE FALTA ---
Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top, double sf) {
return Positioned(
top: top, left: left > 0 ? left : null, right: right > 0 ? right : null,
top: top,
left: left > 0 ? left : null,
right: right > 0 ? right : null,
child: Draggable<String>(
data: action,
feedback: Material(color: Colors.transparent, child: CircleAvatar(radius: 30 * sf, backgroundColor: color.withOpacity(0.8), child: Icon(icon, color: Colors.white, size: 30 * sf))),
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)),
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)),
],
@@ -60,14 +73,20 @@ class _PlacarPageState extends State<PlacarPage> {
);
}
// --- 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,
width: size,
height: size,
child: FloatingActionButton(
heroTag: heroTag, backgroundColor: color,
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),
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),
),
);
}
@@ -77,34 +96,54 @@ class _PlacarPageState extends State<PlacarPage> {
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);
// 👇 CÁLCULO MANUAL DO SF 👇
final 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;
final double cornerBtnSize = 48 * sf; // Tamanho ideal (Nem 38 nem 55)
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(
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,
top: false,
bottom: false,
// 👇 A MÁGICA DO IGNORE POINTER COMEÇA AQUI 👇
child: IgnorePointer(
ignoring: _controller.isSaving,
ignoring: _controller.isSaving, // Se estiver a gravar, ignora os toques!
child: Stack(
children: [
// ==========================================
// --- 1. O CAMPO ---
// ==========================================
// --- 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),
),
decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.5)),
child: LayoutBuilder(
builder: (context, constraints) {
final w = constraints.maxWidth;
@@ -112,19 +151,29 @@ class _PlacarPageState extends State<PlacarPage> {
return Stack(
children: [
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
GestureDetector(
onTapDown: (details) {
if (_controller.isSelectingShotLocation) {
_controller.registerShotLocation(context, details.localPosition, Size(w, h));
}
},
child: Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/campo.png'),
fit: BoxFit.fill,
),
),
child: Stack(
children: _controller.matchShots.map((shot) => Positioned(
left: (shot.relativeX * w) - (9 * sf),
top: (shot.relativeY * h) - (9 * sf),
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)),
// 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(),
),
),
@@ -154,7 +203,7 @@ class _PlacarPageState extends State<PlacarPage> {
// --- BOTÃO PLAY/PAUSE ---
if (!_controller.isSelectingShotLocation)
Positioned(
top: (h * 0.36) + (40 * sf),
top: (h * 0.32) + (40 * sf),
left: 0, right: 0,
child: Center(
child: GestureDetector(
@@ -163,203 +212,112 @@ class _PlacarPageState extends State<PlacarPage> {
radius: 68 * sf,
backgroundColor: Colors.grey.withOpacity(0.5),
child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 58 * sf)
)
)
)
),
),
),
),
// --- PLACAR NO TOPO ---
Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))),
],
);
},
),
),
// ==========================================
// --- 2. O RODAPÉ (BOTÕES DE JOGO) ---
// ==========================================
if (!_controller.isSelectingShotLocation)
// --- BOTÕES DE AÇÃO ---
if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller, sf: sf)),
// --- OVERLAY LANÇAMENTO ---
if (_controller.isSelectingShotLocation)
Positioned(
bottom: 60 * sf,
left: 0,
right: 0,
child: ActionButtonsPanel(controller: _controller, sf: sf)
),
// ==========================================
// --- 3. BOTÕES LATERAIS ---
// ==========================================
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)));
},
top: h * 0.4, left: 0, right: 0,
child: Center(
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))]
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)),
),
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),
),
],
);
},
),
),
// --- BOTÕES LATERAIS ---
// Topo Esquerdo: Guardar e Sair (Botão Único)
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 {
// 1. Primeiro obriga a guardar os dados na BD
await _controller.saveGameStats(context);
// 2. Só depois de acabar de guardar é que volta para trás
if (context.mounted) {
Navigator.pop(context);
}
}
),
),
// Base Esquerda: Banco Casa + TIMEOUT DA CASA
Positioned(
bottom: 55 * sf, left: 12 * sf,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(displayName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? Colors.red : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)),
SizedBox(height: 2 * sf),
Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[700], fontWeight: FontWeight.bold)),
// Texto de faltas com destaque se estiver em perigo (4 ou 5)
Text("AST: ${stats["ast"]} | REB: ${stats["orb"]! + stats["drb"]!} | FALTAS: $fouls",
style: TextStyle(
fontSize: 11 * sf,
color: fouls >= 4 ? Colors.red : Colors.grey[600],
fontWeight: fouls >= 4 ? FontWeight.w900 : FontWeight.w600
)),
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('🛑 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

@@ -3,8 +3,7 @@ import 'package:playmaker/pages/PlacarPage.dart';
import '../controllers/game_controller.dart';
import '../controllers/team_controller.dart';
import '../models/game_model.dart';
import '../utils/size_extension.dart';
import 'dart:math' as math; // 👇 IMPORTANTE PARA O TRAVÃO DE MÃO
import '../utils/size_extension.dart'; // 👇 NOVO SUPERPODER AQUI TAMBÉM!
// --- CARD DE EXIBIÇÃO DO JOGO ---
class GameResultCard extends StatelessWidget {
@@ -19,61 +18,59 @@ class GameResultCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // TRAVÃO DO TABLET
return Container(
margin: EdgeInsets.only(bottom: 16 * safeSf),
padding: EdgeInsets.all(16 * safeSf),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * safeSf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * safeSf)]),
margin: EdgeInsets.only(bottom: 16 * context.sf),
padding: EdgeInsets.all(16 * context.sf),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * context.sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * context.sf)]),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, safeSf)),
_buildScoreCenter(context, gameId, safeSf),
Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, safeSf)),
Expanded(child: _buildTeamInfo(context, myTeam, const Color(0xFFE74C3C), myTeamLogo)),
_buildScoreCenter(context, gameId),
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(
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),
SizedBox(height: 6 * safeSf),
Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * safeSf), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2),
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 * context.sf),
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(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
_scoreBox(myScore, Colors.green, safeSf),
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * safeSf)),
_scoreBox(opponentScore, Colors.grey, safeSf),
_scoreBox(context, myScore, Colors.green),
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * context.sf)),
_scoreBox(context, opponentScore, Colors.grey),
],
),
SizedBox(height: 10 * safeSf),
SizedBox(height: 10 * context.sf),
TextButton.icon(
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)),
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * safeSf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
style: TextButton.styleFrom(backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), padding: EdgeInsets.symmetric(horizontal: 14 * safeSf, vertical: 8 * safeSf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * safeSf)), visualDensity: VisualDensity.compact),
icon: Icon(Icons.play_circle_fill, size: 18 * context.sf, color: const Color(0xFFE74C3C)),
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 * context.sf, vertical: 8 * context.sf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), visualDensity: VisualDensity.compact),
),
SizedBox(height: 6 * safeSf),
Text(status, style: TextStyle(fontSize: 12 * safeSf, color: Colors.blue, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * context.sf),
Text(status, style: TextStyle(fontSize: 12 * context.sf, color: Colors.blue, fontWeight: FontWeight.bold)),
],
);
}
Widget _scoreBox(String pts, Color c, double safeSf) => Container(
padding: EdgeInsets.symmetric(horizontal: 12 * safeSf, vertical: 6 * safeSf),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * safeSf)),
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * safeSf)),
Widget _scoreBox(BuildContext context, String pts, Color c) => Container(
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf, vertical: 6 * context.sf),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * context.sf)),
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
Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // TRAVÃO DO TABLET
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * safeSf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * safeSf)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)),
content: SingleChildScrollView(
child: Container(
constraints: BoxConstraints(maxWidth: 450 * safeSf), // LIMITA A LARGURA NO TABLET
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
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))),
SizedBox(height: 15 * safeSf),
_buildSearch(label: "Minha Equipa", controller: _myTeamController, safeSf: safeSf),
Padding(padding: EdgeInsets.symmetric(vertical: 10 * safeSf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * safeSf))),
_buildSearch(label: "Adversário", controller: _opponentController, safeSf: safeSf),
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))),
SizedBox(height: 15 * context.sf),
_buildSearch(context, "Minha Equipa", _myTeamController),
Padding(padding: EdgeInsets.symmetric(vertical: 10 * context.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * context.sf))),
_buildSearch(context, "Adversário", _opponentController),
],
),
),
),
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(
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 {
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
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>>>(
stream: widget.teamController.teamsStream,
builder: (context, snapshot) {
@@ -164,9 +156,9 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0, borderRadius: BorderRadius.circular(8 * safeSf),
elevation: 4.0, borderRadius: BorderRadius.circular(8 * context.sf),
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(
padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
@@ -174,8 +166,8 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
final String name = option['name'].toString();
final String? imageUrl = option['image_url'];
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),
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * safeSf)),
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 * context.sf)),
onTap: () { onSelected(option); },
);
},
@@ -188,8 +180,8 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) txtCtrl.text = controller.text;
txtCtrl.addListener(() { controller.text = txtCtrl.text; });
return TextField(
controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * safeSf),
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * safeSf), prefixIcon: Icon(Icons.search, size: 20 * safeSf), border: const OutlineInputBorder()),
controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * context.sf),
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 ---
class GamePage extends StatefulWidget {
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 '../utils/size_extension.dart';
import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart';
import 'dart:math' as math; // 👇 IMPORTANTE
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@@ -31,10 +30,10 @@ class _HomeScreenState extends State<HomeScreen> {
@override
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 = [
_buildHomeContent(context, safeSf), // Passamos o safeSf
_buildHomeContent(context), // Passamos só o context
const GamePage(),
const TeamsPage(),
const StatusPage(),
@@ -43,11 +42,11 @@ class _HomeScreenState extends State<HomeScreen> {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * safeSf)),
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)),
backgroundColor: HomeConfig.primaryColor,
foregroundColor: Colors.white,
leading: IconButton(
icon: Icon(Icons.person, size: 24 * safeSf),
icon: Icon(Icons.person, size: 24 * context.sf),
onPressed: () {},
),
),
@@ -63,7 +62,8 @@ class _HomeScreenState extends State<HomeScreen> {
backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
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 [
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'),
@@ -74,16 +74,16 @@ class _HomeScreenState extends State<HomeScreen> {
);
}
void _showTeamSelector(BuildContext context, double safeSf) {
void _showTeamSelector(BuildContext context) {
showModalBottomSheet(
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) {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: _teamController.teamsStream,
builder: (context, snapshot) {
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!;
return ListView.builder(
@@ -92,7 +92,7 @@ class _HomeScreenState extends State<HomeScreen> {
itemBuilder: (context, index) {
final team = teams[index];
return ListTile(
title: Text(team['name'], style: TextStyle(fontSize: 16 * safeSf)),
title: Text(team['name']),
onTap: () {
setState(() {
_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;
// Evita que os cartões fiquem muito altos no tablet:
final double cardHeight = math.min(wScreen * 0.5, 200 * safeSf);
final double cardHeight = wScreen * 0.5;
return StreamBuilder<List<Map<String, dynamic>>>(
stream: _selectedTeamId != null
@@ -126,44 +125,44 @@ class _HomeScreenState extends State<HomeScreen> {
return SingleChildScrollView(
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () => _showTeamSelector(context, safeSf),
onTap: () => _showTeamSelector(context),
child: Container(
padding: EdgeInsets.all(12 * safeSf),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * safeSf), border: Border.all(color: Colors.grey.shade300)),
padding: EdgeInsets.all(12 * context.sf),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.shade300)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
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),
],
),
),
),
SizedBox(height: 20 * safeSf),
SizedBox(height: 20 * context.sf),
SizedBox(
height: cardHeight,
child: Row(
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)),
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))),
],
),
),
SizedBox(height: 12 * safeSf),
SizedBox(height: 12 * context.sf),
SizedBox(
height: cardHeight,
child: Row(
children: [
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(
child: PieChartCard(
victories: _teamWins,
@@ -172,22 +171,22 @@ class _HomeScreenState extends State<HomeScreen> {
title: 'DESEMPENHO',
subtitle: 'Temporada',
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])),
SizedBox(height: 16 * safeSf),
Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[800])),
SizedBox(height: 16 * context.sf),
_selectedTeamName == "Selecionar Equipa"
? Container(
padding: EdgeInsets.all(20 * safeSf),
padding: EdgeInsets.all(20 * context.sf),
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>>>(
stream: _supabase.from('games').stream(primaryKey: ['id'])
@@ -207,7 +206,7 @@ class _HomeScreenState extends State<HomeScreen> {
if (gamesList.isEmpty) {
return Container(
padding: EdgeInsets.all(20 * safeSf),
padding: EdgeInsets.all(20 * context.sf),
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)),
alignment: Alignment.center,
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';
return _buildGameHistoryCard(
context: context,
context: context, // Usamos o context para o sf
opponent: opponent,
result: result,
myScore: myScore,
@@ -248,14 +247,13 @@ class _HomeScreenState extends State<HomeScreen> {
topRbs: game['top_rbs_name'] ?? '---',
topDef: game['top_def_name'] ?? '---',
mvp: game['mvp_name'] ?? '---',
safeSf: safeSf // Passa a escala aqui
);
}).toList(),
);
},
),
SizedBox(height: 20 * safeSf),
SizedBox(height: 20 * context.sf),
],
),
),
@@ -325,14 +323,14 @@ class _HomeScreenState extends State<HomeScreen> {
Widget _buildGameHistoryCard({
required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date,
required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp, required double safeSf
required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp
}) {
bool isWin = result == 'V';
bool isDraw = result == 'E';
Color statusColor = isWin ? Colors.green : (isDraw ? Colors.yellow.shade700 : Colors.red);
return Container(
margin: EdgeInsets.only(bottom: 14 * safeSf),
margin: EdgeInsets.only(bottom: 14 * context.sf),
decoration: BoxDecoration(
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))],
@@ -340,34 +338,34 @@ class _HomeScreenState extends State<HomeScreen> {
child: Column(
children: [
Padding(
padding: EdgeInsets.all(14 * safeSf),
padding: EdgeInsets.all(14 * context.sf),
child: Row(
children: [
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),
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(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(date, style: TextStyle(fontSize: 11 * safeSf, color: Colors.grey, fontWeight: FontWeight.w600)),
SizedBox(height: 6 * safeSf),
Text(date, style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.w600)),
SizedBox(height: 6 * context.sf),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * 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: EdgeInsets.symmetric(horizontal: 8 * safeSf),
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf),
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)),
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),
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))),
child: Column(
children: [
Row(
children: [
Expanded(child: _buildGridStatRow(Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, safeSf, isMvp: true)),
Expanded(child: _buildGridStatRow(Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef, safeSf)),
Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)),
Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef)),
],
),
SizedBox(height: 8 * safeSf),
SizedBox(height: 8 * context.sf),
Row(
children: [
Expanded(child: _buildGridStatRow(Icons.bolt, Colors.blue.shade700, "Pontos", topPts, safeSf)),
Expanded(child: _buildGridStatRow(Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs, safeSf)),
Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)),
Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs)),
],
),
SizedBox(height: 8 * safeSf),
SizedBox(height: 8 * context.sf),
Row(
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()),
],
),
@@ -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(
children: [
Icon(icon, size: 14 * safeSf, color: color),
SizedBox(width: 4 * safeSf),
Text('$label: ', style: TextStyle(fontSize: 11 * safeSf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
Icon(icon, size: 14 * context.sf, color: color),
SizedBox(width: 4 * context.sf),
Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 11 * safeSf,
fontSize: 11 * context.sf,
color: isMvp ? Colors.amber.shade900 : Colors.black87,
fontWeight: FontWeight.bold
),

View File

@@ -1,10 +1,7 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../controllers/team_controller.dart';
import '../utils/size_extension.dart';
import 'dart:math' as math;
import '../controllers/placar_controller.dart'; // Para a classe ShotRecord
import '../pages/heatmap_page.dart'; // Para abrir a página do mapa
import '../utils/size_extension.dart'; // 👇 A MAGIA DO SF!
class StatusPage extends StatefulWidget {
const StatusPage({super.key});
@@ -22,70 +19,19 @@ class _StatusPageState extends State<StatusPage> {
String _sortColumn = 'pts';
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
Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15);
final double screenWidth = MediaQuery.of(context).size.width;
return Column(
children: [
// --- SELETOR DE EQUIPA ---
Padding(
padding: EdgeInsets.all(16.0 * safeSf),
padding: EdgeInsets.all(16.0 * context.sf),
child: InkWell(
onTap: () => _showTeamSelector(context, safeSf),
onTap: () => _showTeamSelector(context),
child: Container(
padding: EdgeInsets.all(12 * safeSf),
padding: EdgeInsets.all(12 * context.sf),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15 * safeSf),
borderRadius: BorderRadius.circular(15 * context.sf),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)]
),
@@ -93,9 +39,9 @@ class _StatusPageState extends State<StatusPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(children: [
Icon(Icons.shield, color: const Color(0xFFE74C3C), size: 24 * safeSf),
SizedBox(width: 10 * safeSf),
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * safeSf, fontWeight: FontWeight.bold))
Icon(Icons.shield, color: const Color(0xFFE74C3C), 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),
],
@@ -104,10 +50,9 @@ class _StatusPageState extends State<StatusPage> {
),
),
// --- TABELA DE ESTATÍSTICAS ---
Expanded(
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>>>(
stream: _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
builder: (context, statsSnapshot) {
@@ -122,7 +67,7 @@ class _StatusPageState extends State<StatusPage> {
}
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 gamesData = gamesSnapshot.data ?? [];
@@ -137,7 +82,7 @@ class _StatusPageState extends State<StatusPage> {
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) {
Map<String, Map<String, dynamic>> aggregated = {};
for (var member in members) {
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};
}
for (var row in stats) {
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};
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) {
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 (defRaw != null) {
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};
}
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, double safeSf, double screenWidth) {
double dynamicSpacing = math.max(15 * safeSf, (screenWidth - (180 * safeSf)) / 8);
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals) {
return Container(
color: Colors.white,
width: double.infinity,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: screenWidth),
child: DataTable(
columnSpacing: dynamicSpacing,
horizontalMargin: 20 * safeSf,
columnSpacing: 25 * context.sf,
headingRowColor: MaterialStateProperty.all(Colors.grey.shade100),
dataRowHeight: 60 * safeSf,
dataRowHeight: 60 * context.sf,
columns: [
DataColumn(label: const Text('JOGADOR')),
_buildSortableColumn('J', 'j', safeSf),
_buildSortableColumn('PTS', 'pts', safeSf),
_buildSortableColumn('AST', 'ast', safeSf),
_buildSortableColumn('RBS', 'rbs', safeSf),
_buildSortableColumn('STL', 'stl', safeSf),
_buildSortableColumn('BLK', 'blk', safeSf),
_buildSortableColumn('DEF 🛡️', 'def', safeSf),
_buildSortableColumn('MVP 🏆', 'mvp', safeSf),
_buildSortableColumn(context, 'J', 'j'),
_buildSortableColumn(context, 'PTS', 'pts'),
_buildSortableColumn(context, 'AST', 'ast'),
_buildSortableColumn(context, 'RBS', 'rbs'),
_buildSortableColumn(context, 'STL', 'stl'),
_buildSortableColumn(context, 'BLK', 'blk'),
_buildSortableColumn(context, 'DEF 🛡️', 'def'),
_buildSortableColumn(context, 'MVP 🏆', 'mvp'),
],
rows: [
...players.map((player) => DataRow(cells: [
DataCell(
// 👇 TORNEI O NOME CLICÁVEL PARA ABRIR O MAPA
InkWell(
onTap: () => _openPlayerHeatmap(player['name']),
child: Row(children: [
CircleAvatar(radius: 15 * safeSf, backgroundColor: Colors.grey.shade200, child: Icon(Icons.person, size: 18 * safeSf)),
SizedBox(width: 10 * safeSf),
Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * safeSf, color: Colors.blue.shade700))
]),
)
),
DataCell(Row(children: [CircleAvatar(radius: 15 * context.sf, backgroundColor: Colors.grey.shade200, child: Icon(Icons.person, size: 18 * context.sf)), SizedBox(width: 10 * context.sf), Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf))])),
DataCell(Center(child: Text(player['j'].toString()))),
_buildStatCell(player['pts'], safeSf, isHighlight: true),
_buildStatCell(player['ast'], safeSf),
_buildStatCell(player['rbs'], safeSf),
_buildStatCell(player['stl'], safeSf),
_buildStatCell(player['blk'], safeSf),
_buildStatCell(player['def'], safeSf, isBlue: true),
_buildStatCell(player['mvp'], safeSf, isGold: true),
_buildStatCell(context, player['pts'], isHighlight: true),
_buildStatCell(context, player['ast']),
_buildStatCell(context, player['rbs']),
_buildStatCell(context, player['stl']),
_buildStatCell(context, player['blk']),
_buildStatCell(context, player['def'], isBlue: true),
_buildStatCell(context, player['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(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.black, fontSize: 12 * context.sf))),
DataCell(Center(child: Text(teamTotals['j'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)))),
_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),
_buildStatCell(context, teamTotals['pts'], isHighlight: true),
_buildStatCell(context, teamTotals['ast']),
_buildStatCell(context, teamTotals['rbs']),
_buildStatCell(context, teamTotals['stl']),
_buildStatCell(context, teamTotals['blk']),
_buildStatCell(context, teamTotals['def'], isBlue: true),
_buildStatCell(context, teamTotals['mvp'], isGold: true),
]
)
],
),
),
),
),
);
}
// (Outras funções de build continuam igual...)
DataColumn _buildSortableColumn(String title, String sortKey, double safeSf) {
DataColumn _buildSortableColumn(BuildContext context, String title, String sortKey) {
return DataColumn(label: InkWell(
onTap: () => setState(() {
if (_sortColumn == sortKey) _isAscending = !_isAscending;
else { _sortColumn = sortKey; _isAscending = false; }
}),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: TextStyle(fontSize: 12 * safeSf, fontWeight: FontWeight.bold)),
if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * safeSf, color: const Color(0xFFE74C3C)),
]
),
child: Row(children: [
Text(title, style: TextStyle(fontSize: 12 * context.sf, fontWeight: FontWeight.bold)),
if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * context.sf, 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(
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)),
child: Text(value == 0 ? "-" : value.toString(), style: TextStyle(
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>>>(
stream: _teamController.teamsStream,
builder: (context, snapshot) {
final teams = snapshot.data ?? [];
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); },
));
},

View File

@@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
import 'package:playmaker/screens/team_stats_page.dart';
import '../controllers/team_controller.dart';
import '../models/team_model.dart';
import '../utils/size_extension.dart';
import 'dart:math' as math;
import '../utils/size_extension.dart'; // 👇 IMPORTANTE: O TEU NOVO SUPERPODER
class TeamsPage extends StatefulWidget {
const TeamsPage({super.key});
@@ -122,6 +121,7 @@ class _TeamsPageState extends State<TeamsPage> {
@override
Widget build(BuildContext context) {
// 🔥 OLHA QUE LIMPEZA: Já não precisamos de calcular nada aqui!
return Scaffold(
backgroundColor: const Color(0xFFF5F7FA),
appBar: AppBar(
@@ -142,7 +142,7 @@ class _TeamsPageState extends State<TeamsPage> {
],
),
floatingActionButton: FloatingActionButton(
heroTag: 'add_team_btn',
heroTag: 'add_team_btn', // 👇 A MÁGICA ESTÁ AQUI!
backgroundColor: const Color(0xFFE74C3C),
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
onPressed: () => _showCreateDialog(context),
@@ -151,33 +151,30 @@ class _TeamsPageState extends State<TeamsPage> {
}
Widget _buildSearchBar() {
final double safeSf = math.min(context.sf, 1.15); // Travão para a barra não ficar com margens gigantes
return Padding(
padding: EdgeInsets.all(16.0 * safeSf),
padding: EdgeInsets.all(16.0 * context.sf),
child: TextField(
controller: _searchController,
onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()),
style: TextStyle(fontSize: 16 * safeSf),
style: TextStyle(fontSize: 16 * context.sf),
decoration: InputDecoration(
hintText: 'Pesquisar equipa...',
hintStyle: TextStyle(fontSize: 16 * safeSf),
prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * safeSf),
hintStyle: TextStyle(fontSize: 16 * context.sf),
prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * context.sf),
filled: true,
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() {
final double safeSf = math.min(context.sf, 1.15);
return StreamBuilder<List<Map<String, dynamic>>>(
stream: controller.teamsStream,
builder: (context, snapshot) {
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!);
@@ -194,7 +191,7 @@ class _TeamsPageState extends State<TeamsPage> {
});
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16 * safeSf), // Margem perfeitamente alinhada
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf),
itemCount: data.length,
itemBuilder: (context, index) {
final team = Team.fromMap(data[index]);
@@ -227,70 +224,68 @@ class TeamCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // O verdadeiro salvador do tablet
return Card(
color: Colors.white, elevation: 3, margin: EdgeInsets.only(bottom: 12 * safeSf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * safeSf)),
color: Colors.white, elevation: 3, margin: EdgeInsets.only(bottom: 12 * context.sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 8 * safeSf),
contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 8 * context.sf),
leading: Stack(
clipBehavior: Clip.none,
children: [
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,
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(
left: -15 * safeSf, top: -10 * safeSf,
left: -15 * context.sf, top: -10 * context.sf,
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,
),
),
],
),
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(
padding: EdgeInsets.only(top: 6.0 * safeSf),
padding: EdgeInsets.only(top: 6.0 * context.sf),
child: Row(
children: [
Icon(Icons.groups_outlined, size: 16 * safeSf, color: Colors.grey),
SizedBox(width: 4 * safeSf),
Icon(Icons.groups_outlined, size: 16 * context.sf, color: Colors.grey),
SizedBox(width: 4 * context.sf),
StreamBuilder<int>(
stream: controller.getPlayerCountStream(team.id),
initialData: 0,
builder: (context, snapshot) {
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),
Expanded(child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * safeSf), overflow: TextOverflow.ellipsis)),
SizedBox(width: 8 * context.sf),
Expanded(child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * context.sf), overflow: TextOverflow.ellipsis)),
],
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
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: 'Eliminar Equipa', icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * safeSf), onPressed: () => _confirmDelete(context, safeSf)),
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 * context.sf), onPressed: () => _confirmDelete(context)),
],
),
),
);
}
void _confirmDelete(BuildContext context, double safeSf) {
void _confirmDelete(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)),
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * safeSf)),
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 * context.sf)),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * safeSf))),
TextButton(onPressed: () { controller.deleteTeam(team.id); Navigator.pop(context); }, child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * 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 * context.sf))),
],
),
);
@@ -313,37 +308,32 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
@override
Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15);
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * safeSf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
content: SingleChildScrollView(
child: Container(
constraints: BoxConstraints(maxWidth: 450 * safeSf), // O popup pode ter um travão para não cobrir a tela toda, fica mais bonito
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(controller: _nameController, style: TextStyle(fontSize: 14 * safeSf), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * safeSf)), textCapitalization: TextCapitalization.words),
SizedBox(height: 15 * safeSf),
TextField(controller: _nameController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * context.sf)), textCapitalization: TextCapitalization.words),
SizedBox(height: 15 * context.sf),
DropdownButtonFormField<String>(
value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * safeSf)),
style: TextStyle(fontSize: 14 * safeSf, color: Colors.black87),
value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf)),
style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87),
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),
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))),
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))),
],
),
),
),
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(
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); } },
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:playmaker/controllers/login_controller.dart';
import 'package:playmaker/pages/RegisterPage.dart';
import '../utils/size_extension.dart';
import 'dart:math' as math; // 👇 IMPORTANTE PARA O TRAVÃO NO TABLET!
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
class BasketTrackHeader extends StatelessWidget {
const BasketTrackHeader({super.key});
@override
Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // TRAVÃO DE MÃO
return Column(
children: [
SizedBox(
width: 200 * safeSf,
height: 200 * safeSf,
width: 200 * context.sf, // Ajusta o tamanho da imagem suavemente
height: 200 * context.sf,
child: Image.asset(
'assets/playmaker-logos.png',
fit: BoxFit.contain,
@@ -24,16 +21,16 @@ class BasketTrackHeader extends StatelessWidget {
Text(
'BasketTrack',
style: TextStyle(
fontSize: 36 * safeSf,
fontSize: 36 * context.sf,
fontWeight: FontWeight.bold,
color: Colors.grey[900],
),
),
SizedBox(height: 6 * safeSf),
SizedBox(height: 6 * context.sf),
Text(
'Gere as tuas equipas e estatísticas',
style: TextStyle(
fontSize: 16 * safeSf,
fontSize: 16 * context.sf,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
@@ -51,42 +48,40 @@ class LoginFormFields extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15);
return Column(
children: [
TextField(
controller: controller.emailController,
style: TextStyle(fontSize: 15 * safeSf),
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'E-mail',
labelStyle: TextStyle(fontSize: 15 * safeSf),
prefixIcon: Icon(Icons.email_outlined, size: 22 * safeSf),
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf),
errorText: controller.emailError,
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),
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 20 * safeSf),
SizedBox(height: 20 * context.sf),
TextField(
controller: controller.passwordController,
obscureText: controller.obscurePassword,
style: TextStyle(fontSize: 15 * safeSf),
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * safeSf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * safeSf),
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf),
errorText: controller.passwordError,
suffixIcon: IconButton(
icon: Icon(
controller.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
size: 22 * safeSf
size: 22 * context.sf
),
onPressed: controller.togglePasswordVisibility,
),
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),
),
),
],
@@ -102,11 +97,9 @@ class LoginButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15);
return SizedBox(
width: double.infinity,
height: 58 * safeSf,
height: 58 * context.sf,
child: ElevatedButton(
onPressed: controller.isLoading ? null : () async {
final success = await controller.login();
@@ -115,15 +108,15 @@ class LoginButton extends StatelessWidget {
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * safeSf)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
elevation: 3,
),
child: controller.isLoading
? 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)),
)
: 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
Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15);
return SizedBox(
width: double.infinity,
height: 58 * safeSf,
height: 58 * context.sf,
child: OutlinedButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterPage()));
},
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFE74C3C),
side: BorderSide(color: const Color(0xFFE74C3C), width: 2 * safeSf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * safeSf)),
side: BorderSide(color: const Color(0xFFE74C3C), width: 2 * context.sf),
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 '../controllers/register_controller.dart';
import '../utils/size_extension.dart';
import 'dart:math' as math; // 👇 IMPORTANTE
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
class RegisterHeader extends StatelessWidget {
const RegisterHeader({super.key});
@override
Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // TRAVÃO
return Column(
children: [
Icon(Icons.person_add_outlined, size: 100 * safeSf, color: const Color(0xFFE74C3C)),
SizedBox(height: 10 * safeSf),
Icon(Icons.person_add_outlined, size: 100 * context.sf, color: const Color(0xFFE74C3C)),
SizedBox(height: 10 * context.sf),
Text(
'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(
'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,
),
],
@@ -42,77 +39,72 @@ class _RegisterFormFieldsState extends State<RegisterFormFields> {
@override
Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // TRAVÃO
return Container(
constraints: BoxConstraints(maxWidth: 450 * safeSf), // LIMITA A LARGURA NO TABLET
child: Form(
return Form(
key: widget.controller.formKey,
child: Column(
children: [
TextFormField(
controller: widget.controller.nameController,
style: TextStyle(fontSize: 15 * safeSf),
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'Nome Completo',
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),
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.person_outline, size: 22 * context.sf),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
),
),
SizedBox(height: 20 * safeSf),
SizedBox(height: 20 * context.sf),
TextFormField(
controller: widget.controller.emailController,
validator: widget.controller.validateEmail,
style: TextStyle(fontSize: 15 * safeSf),
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'E-mail',
labelStyle: TextStyle(fontSize: 15 * safeSf),
prefixIcon: Icon(Icons.email_outlined, size: 22 * safeSf),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * safeSf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * safeSf, horizontal: 16 * safeSf),
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 20 * safeSf),
SizedBox(height: 20 * context.sf),
TextFormField(
controller: widget.controller.passwordController,
obscureText: _obscurePassword,
validator: widget.controller.validatePassword,
style: TextStyle(fontSize: 15 * safeSf),
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * safeSf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * safeSf),
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf),
suffixIcon: IconButton(
icon: Icon(_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 22 * safeSf),
icon: Icon(_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 22 * context.sf),
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(
controller: widget.controller.confirmPasswordController,
obscureText: _obscurePassword,
validator: widget.controller.validateConfirmPassword,
style: TextStyle(fontSize: 15 * safeSf),
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'Confirmar Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * safeSf),
prefixIcon: Icon(Icons.lock_clock_outlined, size: 22 * safeSf),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * safeSf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * safeSf, horizontal: 16 * safeSf),
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.lock_clock_outlined, size: 22 * context.sf),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
),
),
],
),
),
);
}
}
@@ -123,25 +115,23 @@ class RegisterButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double safeSf = math.min(context.sf, 1.15); // TRAVÃO
return Container(
constraints: BoxConstraints(maxWidth: 450 * safeSf), // LIMITA LARGURA
height: 58 * safeSf,
return SizedBox(
width: double.infinity,
height: 58 * context.sf,
child: ElevatedButton(
onPressed: controller.isLoading ? null : () => controller.signUp(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * safeSf)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
elevation: 3,
),
child: controller.isLoading
? 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)),
)
: 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 '../models/team_model.dart';
import '../controllers/team_controller.dart';
import 'dart:math' as math; // 👇 IMPORTANTE PARA O TRAVÃO DE MÃO
class TeamCard extends StatelessWidget {
final Team team;
final TeamController controller;
final VoidCallback onFavoriteTap;
final double sf; // <-- Variável de escala original
final double sf; // <-- Variável de escala
const TeamCard({
super.key,
@@ -20,24 +19,20 @@ class TeamCard extends StatelessWidget {
@override
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(
color: Colors.white,
elevation: 3,
margin: EdgeInsets.only(bottom: 12 * safeSf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * safeSf)),
margin: EdgeInsets.only(bottom: 12 * sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)),
child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * safeSf, vertical: 8 * safeSf),
contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf),
// --- 1. IMAGEM + FAVORITO ---
leading: Stack(
clipBehavior: Clip.none,
children: [
CircleAvatar(
radius: 28 * safeSf,
radius: 28 * sf,
backgroundColor: Colors.grey[200],
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
? NetworkImage(team.imageUrl)
@@ -45,22 +40,22 @@ class TeamCard extends StatelessWidget {
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
? Text(
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
style: TextStyle(fontSize: 24 * safeSf),
style: TextStyle(fontSize: 24 * sf),
)
: null,
),
Positioned(
left: -15 * safeSf,
top: -10 * safeSf,
left: -15 * sf,
top: -10 * sf,
child: IconButton(
icon: Icon(
team.isFavorite ? Icons.star : Icons.star_border,
color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1),
size: 28 * safeSf,
size: 28 * sf,
shadows: [
Shadow(
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 ---
title: Text(
team.name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * safeSf),
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf),
overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos
),
// --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) ---
subtitle: Padding(
padding: EdgeInsets.only(top: 6.0 * safeSf),
padding: EdgeInsets.only(top: 6.0 * sf),
child: Row(
children: [
Icon(Icons.groups_outlined, size: 16 * safeSf, color: Colors.grey),
SizedBox(width: 4 * safeSf),
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
SizedBox(width: 4 * sf),
// 👇 A CORREÇÃO ESTÁ AQUI: StreamBuilder em vez de FutureBuilder 👇
StreamBuilder<int>(
stream: controller.getPlayerCountStream(team.id),
initialData: 0,
builder: (context, snapshot) {
final count = snapshot.data ?? 0;
return Text(
"$count Jogs.",
"$count Jogs.", // Abreviado para poupar espaço
style: TextStyle(
color: count > 0 ? Colors.green[700] : Colors.orange,
fontWeight: FontWeight.bold,
fontSize: 13 * safeSf,
fontSize: 13 * sf,
),
);
},
),
SizedBox(width: 8 * safeSf),
Expanded(
SizedBox(width: 8 * sf),
Expanded( // Garante que a temporada se adapta se faltar espaço
child: Text(
"| ${team.season}",
style: TextStyle(color: Colors.grey, fontSize: 13 * safeSf),
style: TextStyle(color: Colors.grey, fontSize: 13 * sf),
overflow: TextOverflow.ellipsis,
),
),
@@ -115,11 +111,11 @@ class TeamCard extends StatelessWidget {
// --- 4. BOTÕES (Estatísticas e Apagar) ---
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, // <-- ISTO RESOLVE O OVERFLOW DAS RISCAS AMARELAS
children: [
IconButton(
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: () {
Navigator.push(
context,
@@ -131,8 +127,8 @@ class TeamCard extends StatelessWidget {
),
IconButton(
tooltip: 'Eliminar Equipa',
icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * safeSf),
onPressed: () => _confirmDelete(context, safeSf),
icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * sf),
onPressed: () => _confirmDelete(context),
),
],
),
@@ -140,23 +136,23 @@ class TeamCard extends StatelessWidget {
);
}
void _confirmDelete(BuildContext context, double safeSf) {
void _confirmDelete(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)),
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * safeSf)),
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 * sf)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * safeSf)),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf)),
),
TextButton(
onPressed: () {
controller.deleteTeam(team.id);
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 ---
class CreateTeamDialog extends StatefulWidget {
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});
@@ -182,65 +178,58 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
@override
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(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * safeSf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * safeSf, fontWeight: FontWeight.bold)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold)),
content: SingleChildScrollView(
child: Container(
// 👇 Limita a largura máxima no tablet para o popup não ficar super esticado!
constraints: BoxConstraints(maxWidth: 450 * safeSf),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _nameController,
style: TextStyle(fontSize: 14 * safeSf),
style: TextStyle(fontSize: 14 * widget.sf),
decoration: InputDecoration(
labelText: 'Nome da Equipa',
labelStyle: TextStyle(fontSize: 14 * safeSf)
labelStyle: TextStyle(fontSize: 14 * widget.sf)
),
textCapitalization: TextCapitalization.words,
),
SizedBox(height: 15 * safeSf),
SizedBox(height: 15 * widget.sf),
DropdownButtonFormField<String>(
value: _selectedSeason,
decoration: InputDecoration(
labelText: 'Temporada',
labelStyle: TextStyle(fontSize: 14 * safeSf)
labelStyle: TextStyle(fontSize: 14 * widget.sf)
),
style: TextStyle(fontSize: 14 * safeSf, color: Colors.black87),
style: TextStyle(fontSize: 14 * widget.sf, color: Colors.black87),
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),
SizedBox(height: 15 * widget.sf),
TextField(
controller: _imageController,
style: TextStyle(fontSize: 14 * safeSf),
style: TextStyle(fontSize: 14 * widget.sf),
decoration: InputDecoration(
labelText: 'URL Imagem ou Emoji',
labelStyle: TextStyle(fontSize: 14 * safeSf),
labelStyle: TextStyle(fontSize: 14 * widget.sf),
hintText: 'Ex: 🏀 ou https://...',
hintStyle: TextStyle(fontSize: 14 * safeSf)
hintStyle: TextStyle(fontSize: 14 * widget.sf)
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * safeSf))
child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf))
),
ElevatedButton(
style: ElevatedButton.styleFrom(
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: () {
if (_nameController.text.trim().isNotEmpty) {
@@ -252,7 +241,7 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
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
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
clock:
dependency: transitive
description:
@@ -268,18 +268,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
@@ -553,10 +553,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
version: "0.7.7"
version: "0.7.9"
typed_data:
dependency: transitive
description: