Compare commits

..

2 Commits

Author SHA1 Message Date
c2619fe6d6 Merge branch 'main' of https://git.epvc.pt/230404/PlayMaker 2026-03-16 15:28:26 +00:00
3dbccdc823 dividir por zona de pontos 2026-03-09 15:05:14 +00:00
30 changed files with 1801 additions and 2735 deletions

View File

@@ -9,7 +9,7 @@ android {
namespace = "com.example.playmaker"
compileSdk = flutter.compileSdkVersion
//ndkVersion = flutter.ndkVersion
ndkVersion = "27.0.12077973"
ndkVersion = "26.1.10909125"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

View File

@@ -1,208 +0,0 @@
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

@@ -1,115 +0,0 @@
import 'package:flutter/material.dart';
class AppTheme {
static const Color primaryRed = Color(0xFFE74C3C);
static const Color backgroundLight = Color(0xFFF5F7FA);
static const Color surfaceWhite = Colors.white;
static const Color successGreen = Color(0xFF00C853);
static const Color warningAmber = Colors.amber;
static const Color placarBackground = Color(0xFF266174);
static const Color placarDarkSurface = Color(0xFF16202C);
static const Color placarTimerBg = Color(0xFF2C3E50);
static const Color placarListCard = Color(0xFF263238);
static const Color myTeamBlue = Color(0xFF1E5BB2);
static const Color oppTeamRed = Color(0xFFD92C2C);
static const Color actionPoints = Colors.orange;
static const Color actionMiss = Colors.redAccent;
static const Color actionSteal = Colors.green;
static const Color actionAssist = Colors.blueGrey;
static const Color actionRebound = Color(0xFF1E2A38);
static const Color actionBlock = Colors.deepPurple;
static const Color statPtsBg = Color(0xFF1565C0);
static const Color statAstBg = Color(0xFF2E7D32);
static const Color statRebBg = Color(0xFF6A1B9A);
static const Color statPieBg = Color.fromARGB(255, 22, 32, 44);
static const Color coachBg = Color(0xFFFFF9C4);
// =========================================================
// ☀️ TEMA CLARO
// =========================================================
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryRed,
brightness: Brightness.light,
primary: primaryRed,
surface: backgroundLight,
),
appBarTheme: const AppBarTheme(
backgroundColor: backgroundLight,
foregroundColor: Colors.black87,
centerTitle: true,
elevation: 0.0,
),
// 👇 CORRETO: Classe CardThemeData
cardTheme: const CardThemeData(
color: surfaceWhite,
surfaceTintColor: Colors.transparent, // Evita o tom rosado do Material 3
elevation: 3.0,
margin: EdgeInsets.only(bottom: 12.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
side: BorderSide(color: Color(0xFFEEEEEE), width: 1.0),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: surfaceWhite,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
),
),
);
}
// =========================================================
// 🌙 MODO ESCURO
// =========================================================
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryRed,
brightness: Brightness.dark,
primary: primaryRed,
surface: const Color(0xFF1E1E1E),
),
scaffoldBackgroundColor: const Color(0xFF121212),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF121212),
foregroundColor: Colors.white,
centerTitle: true,
elevation: 0.0,
),
// 👇 CORRETO: Classe CardThemeData
cardTheme: const CardThemeData(
color: Color(0xFF1E1E1E),
surfaceTintColor: Colors.transparent,
elevation: 3.0,
margin: EdgeInsets.only(bottom: 12.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
side: BorderSide(color: Color(0xFF2C2C2C), width: 1.0),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFF1E1E1E),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(color: Color(0xFF2C2C2C)),
),
),
);
}
}

View File

@@ -4,34 +4,25 @@ import '../models/game_model.dart';
class GameController {
final _supabase = Supabase.instance.client;
// 1. LER JOGOS (Com Filtros Opcionais)
Stream<List<Game>> getFilteredGames({String? teamFilter, String? seasonFilter}) {
// 1. LER JOGOS (Stream em Tempo Real)
Stream<List<Game>> get gamesStream {
return _supabase
.from('games')
.from('games') // 1. Fica à escuta da tabela original (Garante o Tempo Real!)
.stream(primaryKey: ['id'])
.asyncMap((event) async {
// 👇 A CORREÇÃO ESTÁ AQUI: Lê diretamente da tabela 'games'
var query = _supabase.from('games').select();
// Aplica o filtro de Temporada
if (seasonFilter != null && seasonFilter.isNotEmpty && seasonFilter != 'Todas') {
query = query.eq('season', seasonFilter);
}
// Aplica o filtro de Equipa (Procura em casa ou fora)
if (teamFilter != null && teamFilter.isNotEmpty && teamFilter != 'Todas') {
query = query.or('my_team.eq.$teamFilter,opponent_team.eq.$teamFilter');
}
// Executa a query com a ordenação por data
final viewData = await query.order('game_date', ascending: false);
// 2. Sempre que a tabela 'games' mudar (novo jogo, alteração de resultado),
// vamos buscar os dados já misturados com as imagens à nossa View.
final viewData = await _supabase
.from('games_with_logos')
.select()
.order('game_date', ascending: false);
// 3. Convertemos para a nossa lista de objetos Game
return viewData.map((json) => Game.fromMap(json)).toList();
});
}
// 2. CRIAR JOGO
// Retorna o ID do jogo criado para podermos navegar para o placar
Future<String?> createGame(String myTeam, String opponent, String season) async {
try {
final response = await _supabase.from('games').insert({
@@ -40,16 +31,18 @@ class GameController {
'season': season,
'my_score': 0,
'opponent_score': 0,
'status': 'Decorrer',
'status': 'Decorrer', // Começa como "Decorrer"
'game_date': DateTime.now().toIso8601String(),
}).select().single();
}).select().single(); // .select().single() retorna o objeto criado
return response['id'];
return response['id']; // Retorna o UUID gerado pelo Supabase
} catch (e) {
print("Erro ao criar jogo: $e");
return null;
}
}
void dispose() {}
void dispose() {
// Não é necessário fechar streams do Supabase manualmente aqui
}
}

View File

@@ -1,20 +1,11 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class ShotRecord {
final double relativeX;
final double relativeY;
final Offset position;
final bool isMake;
final String playerName;
ShotRecord({
required this.relativeX,
required this.relativeY,
required this.isMake,
required this.playerName
});
ShotRecord(this.position, this.isMake);
}
class PlacarController {
@@ -32,8 +23,6 @@ class PlacarController {
bool isLoading = true;
bool isSaving = false;
bool gameWasAlreadyFinished = false;
int myScore = 0;
int opponentScore = 0;
@@ -67,17 +56,13 @@ class PlacarController {
Timer? timer;
bool isRunning = false;
// 👇 VARIÁVEIS DE CALIBRAÇÃO DO CAMPO (OS TEUS NÚMEROS!) 👇
bool isCalibrating = false;
double hoopBaseX = 0.088;
double arcRadius = 0.459;
double cornerY = 0.440;
// --- 🔄 CARREGAMENTO COMPLETO (DADOS REAIS + ESTATÍSTICAS SALVAS) ---
Future<void> loadPlayers() async {
final supabase = Supabase.instance.client;
try {
await Future.delayed(const Duration(milliseconds: 1500));
// 1. Limpar estados para evitar duplicação
myCourt.clear();
myBench.clear();
oppCourt.clear();
@@ -88,6 +73,7 @@ class PlacarController {
myFouls = 0;
opponentFouls = 0;
// 2. Buscar dados básicos do JOGO
final gameResponse = await supabase.from('games').select().eq('id', gameId).single();
myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0;
@@ -99,23 +85,25 @@ class PlacarController {
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;
gameWasAlreadyFinished = gameResponse['status'] == 'Terminado';
// 3. Buscar os IDs das equipas
final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
for (var t in teamsResponse) {
if (t['name'] == myTeam) myTeamDbId = t['id'];
if (t['name'] == opponentTeam) oppTeamDbId = t['id'];
}
// 4. Buscar os Jogadores
List<dynamic> myPlayers = myTeamDbId != null ? await supabase.from('members').select().eq('team_id', myTeamDbId!).eq('type', 'Jogador') : [];
List<dynamic> oppPlayers = oppTeamDbId != null ? await supabase.from('members').select().eq('team_id', oppTeamDbId!).eq('type', 'Jogador') : [];
// 5. BUSCAR ESTATÍSTICAS JÁ SALVAS
final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId);
final Map<String, dynamic> savedStats = {
for (var item in statsResponse) item['member_id'].toString(): item
};
// 6. Registar a tua equipa
for (int i = 0; i < myPlayers.length; i++) {
String dbId = myPlayers[i]['id'].toString();
String name = myPlayers[i]['name'].toString();
@@ -128,13 +116,13 @@ class PlacarController {
"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);
}
}
_padTeam(myCourt, myBench, "Jogador", isMyTeam: true);
// 7. Registar a equipa adversária
for (int i = 0; i < oppPlayers.length; i++) {
String dbId = oppPlayers[i]['id'].toString();
String name = oppPlayers[i]['name'].toString();
@@ -147,7 +135,6 @@ class PlacarController {
"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);
}
@@ -169,11 +156,7 @@ 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
};
playerStats[name] = {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0};
if (isMyTeam) {
if (isCourt) myCourt.add(name); else myBench.add(name);
@@ -188,6 +171,7 @@ class PlacarController {
}
}
// --- TEMPO E TIMEOUTS ---
void toggleTimer(BuildContext context) {
if (isRunning) {
timer?.cancel();
@@ -198,17 +182,15 @@ class PlacarController {
} else {
timer.cancel();
isRunning = false;
if (currentQuarter < 4) {
currentQuarter++;
duration = const Duration(minutes: 10);
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));
}
if (currentQuarter < 4) {
currentQuarter++;
duration = const Duration(minutes: 10);
myFouls = 0;
opponentFouls = 0;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas resetadas!'), backgroundColor: Colors.blue));
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO!'), backgroundColor: Colors.red));
}
}
onUpdate();
});
@@ -230,6 +212,7 @@ class PlacarController {
String formatTime() => "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
// --- LÓGICA DE JOGO & VALIDAÇÃO GEOMÉTRICA DE ZONAS ---
void handleActionDrag(BuildContext context, String action, String playerData) {
String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
final stats = playerStats[name]!;
@@ -273,41 +256,32 @@ class PlacarController {
onUpdate();
}
// =========================================================================
// 👇 A MÁGICA DOS PONTOS ACONTECE AQUI 👇
// =========================================================================
// AGORA RECEBE CONTEXT E SIZE PARA A MATEMÁTICA
void registerShotLocation(BuildContext context, Offset position, Size size) {
if (pendingAction == null || pendingPlayer == null) return;
bool is3Pt = pendingAction!.contains("_3");
bool is2Pt = pendingAction!.contains("_2");
// O ÁRBITRO MATEMÁTICO COM AS TUAS VARIÁVEIS CALIBRADAS
// Validação
if (is3Pt || is2Pt) {
bool isValid = _validateShotZone(position, size, is3Pt);
// SE A JOGADA FOI NO SÍTIO ERRADO
if (!isValid) {
return; // <-- ESTE RETURN BLOQUEIA A GRAVAÇÃO DO PONTO!
// Se a validação falhar, fudeo. Bloqueia.
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(' Local de lançamento incompatível com a pontuação.'),
backgroundColor: Colors.red,
duration: Duration(seconds: 2),
)
);
return; // Aborta!
}
}
// 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(position, isMake));
commitStat(pendingAction!, pendingPlayer!);
isSelectingShotLocation = false;
@@ -316,39 +290,37 @@ class PlacarController {
onUpdate();
}
bool _validateShotZone(Offset position, Size size, bool is3Pt) {
double relX = position.dx / size.width;
double relY = position.dy / size.height;
// A MATEMÁTICA DA ZONA
bool _validateShotZone(Offset pos, Size size, bool is3Pt) {
double w = size.width;
double h = size.height;
bool isLeftHalf = relX < 0.5;
double hoopX = isLeftHalf ? hoopBaseX : (1.0 - hoopBaseX);
double hoopY = 0.50;
// Ajusta o 0.12 e 0.88 se os teus cestos na imagem estiverem mais para o lado
Offset leftHoop = Offset(w * 0.12, h * 0.5);
Offset rightHoop = Offset(w * 0.88, h * 0.5);
double aspectRatio = size.width / size.height;
double distFromCenterY = (relY - hoopY).abs();
// O raio da linha de 3 pontos (Brinca com este 0.28 se a área ficar muito grande ou pequena)
double threePointRadius = w * 0.28;
bool isInside2Pts;
Offset activeHoop = pos.dx < w / 2 ? leftHoop : rightHoop;
double distanceToHoop = (pos - activeHoop).distance;
// 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));
isInside2Pts = distanceToHoop < arcRadius;
// Zonas de canto (onde a linha de 3 é reta)
bool isCorner3 = (pos.dy < h * 0.15 || pos.dy > h * 0.85) &&
(pos.dx < w * 0.20 || pos.dx > w * 0.80);
if (is3Pt) {
return distanceToHoop >= threePointRadius || isCorner3;
} else {
return distanceToHoop < threePointRadius && !isCorner3;
}
if (is3Pt) return !isInside2Pts;
return isInside2Pts;
}
// 👆 ===================================================================== 👆
void cancelShotLocation() {
isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate();
isSelectingShotLocation = false;
pendingAction = null;
pendingPlayer = null;
onUpdate();
}
void commitStat(String action, String playerData) {
@@ -361,7 +333,6 @@ class PlacarController {
if (isOpponent) opponentScore += pts; else myScore += pts;
stats["pts"] = stats["pts"]! + pts;
if (pts == 2 || pts == 3) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; }
if (pts == 1) { stats["ftm"] = stats["ftm"]! + 1; stats["fta"] = stats["fta"]! + 1; }
}
else if (action.startsWith("sub_pts_")) {
int pts = int.parse(action.split("_").last);
@@ -372,15 +343,9 @@ class PlacarController {
if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1;
if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1;
}
if (pts == 1) {
if (stats["ftm"]! > 0) stats["ftm"] = stats["ftm"]! - 1;
if (stats["fta"]! > 0) stats["fta"] = stats["fta"]! - 1;
}
}
else if (action == "miss_1") { stats["fta"] = stats["fta"]! + 1; }
else if (action == "miss_2" || action == "miss_3") { stats["fga"] = stats["fga"]! + 1; }
else if (action == "add_orb") { stats["orb"] = stats["orb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; }
else if (action == "add_drb") { stats["drb"] = stats["drb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; }
else if (action == "add_rbs") { stats["rbs"] = stats["rbs"]! + 1; }
else if (action == "add_ast") { stats["ast"] = stats["ast"]! + 1; }
else if (action == "add_stl") { stats["stl"] = stats["stl"]! + 1; }
else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; }
@@ -395,38 +360,13 @@ class PlacarController {
}
}
// --- 💾 FUNÇÃO PARA GUARDAR DADOS NA BD ---
Future<void> saveGameStats(BuildContext context) async {
final supabase = Supabase.instance.client;
isSaving = true;
onUpdate();
try {
bool isGameFinishedNow = currentQuarter >= 4 && duration.inSeconds == 0;
String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado';
String topPtsName = '---'; int maxPts = -1;
String topAstName = '---'; int maxAst = -1;
String topRbsName = '---'; int maxRbs = -1;
String topDefName = '---'; int maxDef = -1;
String mvpName = '---'; int maxMvpScore = -1;
playerStats.forEach((playerName, stats) {
int pts = stats['pts'] ?? 0;
int ast = stats['ast'] ?? 0;
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)'; }
if (defScore > maxDef && defScore > 0) { maxDef = defScore; topDefName = '$playerName ($defScore)'; }
if (mvpScore > maxMvpScore && mvpScore > 0) { maxMvpScore = mvpScore; mvpName = playerName; }
});
await supabase.from('games').update({
'my_score': myScore,
'opponent_score': opponentScore,
@@ -434,67 +374,43 @@ class PlacarController {
'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,
'status': currentQuarter >= 4 && duration.inSeconds == 0 ? 'Terminado' : 'Pausado',
}).eq('id', gameId);
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) 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 {
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;
}
List<Map<String, dynamic>> batchStats = [];
playerStats.forEach((playerName, stats) {
String? memberDbId = playerDbIds[playerName];
if (memberDbId != null && stats.values.any((val) => val > 0)) {
bool isMyTeamPlayer = myCourt.contains(playerName) || myBench.contains(playerName);
String teamId = isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!;
batchStats.add({
'game_id': gameId, 'member_id': memberDbId, 'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!,
'pts': stats['pts'], 'rbs': stats['rbs'], 'ast': stats['ast'], 'stl': stats['stl'], 'blk': stats['blk'], 'tov': stats['tov'], 'fls': stats['fls'], 'fgm': stats['fgm'], 'fga': stats['fga'], 'ftm': stats['ftm'], 'fta': stats['fta'], 'orb': stats['orb'], 'drb': stats['drb'],
'game_id': gameId,
'member_id': memberDbId,
'team_id': teamId,
'pts': stats['pts'],
'rbs': stats['rbs'],
'ast': stats['ast'],
'stl': stats['stl'],
'blk': stats['blk'],
'tov': stats['tov'],
'fls': stats['fls'],
'fgm': stats['fgm'],
'fga': stats['fga'],
});
}
});
await supabase.from('player_stats').delete().eq('game_id', gameId);
if (batchStats.isNotEmpty) {
await supabase.from('player_stats').insert(batchStats);
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas e Resultados guardados com Sucesso!'), backgroundColor: Colors.green));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas guardadas com Sucesso!'), backgroundColor: Colors.green));
}
} catch (e) {

View File

@@ -0,0 +1,158 @@
/*import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../models/person_model.dart';
class StatsController {
final SupabaseClient _supabase = Supabase.instance.client;
// 1. LER
Stream<List<Person>> getMembers(String teamId) {
return _supabase
.from('members')
.stream(primaryKey: ['id'])
.eq('team_id', teamId)
.order('name', ascending: true)
.map((data) => data.map((json) => Person.fromMap(json)).toList());
}
// 2. APAGAR
Future<void> deletePerson(String personId) async {
try {
await _supabase.from('members').delete().eq('id', personId);
} catch (e) {
debugPrint("Erro ao eliminar: $e");
}
}
// 3. DIÁLOGOS
void showAddPersonDialog(BuildContext context, String teamId) {
_showForm(context, teamId: teamId);
}
void showEditPersonDialog(BuildContext context, String teamId, Person person) {
_showForm(context, teamId: teamId, person: person);
}
// --- O POPUP ESTÁ AQUI ---
void _showForm(BuildContext context, {required String teamId, Person? person}) {
final isEdit = person != null;
final nameCtrl = TextEditingController(text: person?.name ?? '');
final numCtrl = TextEditingController(text: person?.number ?? '');
// Define o valor inicial
String selectedType = person?.type ?? 'Jogador';
showDialog(
context: context,
builder: (ctx) => StatefulBuilder(
builder: (ctx, setState) => AlertDialog(
title: Text(isEdit ? "Editar" : "Adicionar"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
// NOME
TextField(
controller: nameCtrl,
decoration: const InputDecoration(labelText: "Nome"),
textCapitalization: TextCapitalization.sentences,
),
const SizedBox(height: 10),
// FUNÇÃO
DropdownButtonFormField<String>(
value: selectedType,
decoration: const InputDecoration(labelText: "Função"),
items: ["Jogador", "Treinador"]
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
.toList(),
onChanged: (v) {
if (v != null) setState(() => selectedType = v);
},
),
// NÚMERO (Só aparece se for Jogador)
if (selectedType == "Jogador") ...[
const SizedBox(height: 10),
TextField(
controller: numCtrl,
decoration: const InputDecoration(labelText: "Número da Camisola"),
keyboardType: TextInputType.text, // Aceita texto para evitar erros
),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text("Cancelar")
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF00C853)),
onPressed: () async {
print("--- 1. CLICOU EM GUARDAR ---");
// Validação Simples
if (nameCtrl.text.trim().isEmpty) {
print("ERRO: Nome vazio");
return;
}
// Lógica do Número:
// Se for Treinador -> envia NULL
// Se for Jogador e estiver vazio -> envia NULL
// Se tiver texto -> envia o Texto
String? numeroFinal;
if (selectedType == "Treinador") {
numeroFinal = null;
} else {
numeroFinal = numCtrl.text.trim().isEmpty ? null : numCtrl.text.trim();
}
print("--- 2. DADOS A ENVIAR ---");
print("Nome: ${nameCtrl.text}");
print("Tipo: $selectedType");
print("Número: $numeroFinal");
try {
if (isEdit) {
await _supabase.from('members').update({
'name': nameCtrl.text.trim(),
'type': selectedType,
'number': numeroFinal,
}).eq('id', person!.id);
} else {
await _supabase.from('members').insert({
'team_id': teamId, // Verifica se este teamId é válido!
'name': nameCtrl.text.trim(),
'type': selectedType,
'number': numeroFinal,
});
}
print("--- 3. SUCESSO! FECHANDO DIÁLOGO ---");
if (ctx.mounted) Navigator.pop(ctx);
} catch (e) {
print("--- X. ERRO AO GUARDAR ---");
print(e.toString());
// MOSTRA O ERRO NO TELEMÓVEL
if (ctx.mounted) {
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(
content: Text("Erro: $e"),
backgroundColor: Colors.red,
duration: const Duration(seconds: 4),
),
);
}
}
},
child: const Text("Guardar", style: TextStyle(color: Colors.white)),
)
],
),
),
);
}
}*/

View File

@@ -1,24 +1,21 @@
import 'package:supabase_flutter/supabase_flutter.dart';
class TeamController {
// Instância do cliente Supabase
final _supabase = Supabase.instance.client;
// 1. Variável fixa para guardar o Stream principal
late final Stream<List<Map<String, dynamic>>> teamsStream;
// 2. Dicionário (Cache) para não recriar Streams de contagem repetidos
final Map<String, Stream<int>> _playerCountStreams = {};
TeamController() {
// INICIALIZAÇÃO: O stream é criado APENAS UMA VEZ quando abres a página!
teamsStream = _supabase
// 1. STREAM (Realtime)
// Adicionei o .map() no final para garantir que o Dart entende que é uma List<Map>
Stream<List<Map<String, dynamic>>> get teamsStream {
return _supabase
.from('teams')
.stream(primaryKey: ['id'])
.order('name', ascending: true)
.map((data) => List<Map<String, dynamic>>.from(data));
}
// CRIAR
// 2. CRIAR
// Alterei imageUrl para String? (pode ser nulo) para evitar erros se não houver imagem
Future<void> createTeam(String name, String season, String? imageUrl) async {
try {
await _supabase.from('teams').insert({
@@ -33,50 +30,42 @@ class TeamController {
}
}
// ELIMINAR
// 3. ELIMINAR
Future<void> deleteTeam(String id) async {
try {
await _supabase.from('teams').delete().eq('id', id);
// Limpa o cache deste teamId se a equipa for apagada
_playerCountStreams.remove(id);
} catch (e) {
print("❌ Erro ao eliminar: $e");
}
}
// FAVORITAR
// 4. FAVORITAR
Future<void> toggleFavorite(String teamId, bool currentStatus) async {
try {
await _supabase
.from('teams')
.update({'is_favorite': !currentStatus})
.update({'is_favorite': !currentStatus}) // Inverte o valor
.eq('id', teamId);
} catch (e) {
print("❌ Erro ao favoritar: $e");
}
}
// CONTAR JOGADORES (AGORA COM CACHE DE MEMÓRIA!)
Stream<int> getPlayerCountStream(String teamId) {
// Se já criámos um "Tubo de ligação" para esta equipa, REUTILIZA-O!
if (_playerCountStreams.containsKey(teamId)) {
return _playerCountStreams[teamId]!;
// 5. CONTAR JOGADORES
// CORRIGIDO: A sintaxe antiga dava erro. O método .count() é o correto agora.
Future<int> getPlayerCount(String teamId) async {
try {
final count = await _supabase
.from('members')
.count() // Retorna diretamente o número inteiro
.eq('team_id', teamId);
return count;
} catch (e) {
print("Erro ao contar jogadores: $e");
return 0;
}
// Se é a primeira vez que pede esta equipa, cria a ligação e guarda na memória
final newStream = _supabase
.from('members')
.stream(primaryKey: ['id'])
.eq('team_id', teamId)
.map((data) => data.length);
_playerCountStreams[teamId] = newStream; // Guarda no dicionário
return newStream;
}
// LIMPEZA FINAL QUANDO SAÍMOS DA PÁGINA
void dispose() {
// Limpamos o dicionário de streams para libertar memória RAM
_playerCountStreams.clear();
}
// Mantemos o dispose vazio para não quebrar a chamada na TeamsPage
void dispose() {}
}

View File

@@ -1,21 +1,29 @@
import 'package:flutter/material.dart';
import '../dados_grafico.dart'; // Ajusta o caminho se der erro de import
import 'package:flutter/material.dart';
import 'package:playmaker/grafico%20de%20pizza/dados_grafico.dart';
class PieChartController extends ChangeNotifier {
PieChartData _chartData = const PieChartData(victories: 0, defeats: 0, draws: 0);
PieChartData get chartData => _chartData;
void updateData({int? victories, int? defeats, int? draws}) {
_chartData = PieChartData(
victories: victories ?? _chartData.victories,
defeats: defeats ?? _chartData.defeats,
draws: draws ?? _chartData.draws, // 👇 AGORA ELE ACEITA OS EMPATES
);
notifyListeners();
}
void reset() {
updateData(victories: 0, defeats: 0, draws: 0);
}
}
class PieChartController extends ChangeNotifier {
PieChartData _chartData = PieChartData(victories: 25, defeats: 10);
PieChartData get chartData => _chartData;
void updateData({int? victories, int? defeats, int? draws}) {
_chartData = PieChartData(
victories: victories ?? _chartData.victories,
defeats: defeats ?? _chartData.defeats,
draws: draws ?? _chartData.draws,
);
notifyListeners();
}
void incrementVictories() {
updateData(victories: _chartData.victories + 1);
}
void incrementDefeats() {
updateData(defeats: _chartData.defeats + 1);
}
void reset() {
updateData(victories: 0, defeats: 0, draws: 0);
}
}

View File

@@ -1,7 +1,7 @@
class PieChartData {
final int victories;
final int defeats;
final int draws; // 👇 AQUI ESTÃO OS EMPATES
final int draws;
const PieChartData({
required this.victories,
@@ -9,7 +9,6 @@ class PieChartData {
this.draws = 0,
});
// 👇 MATEMÁTICA ATUALIZADA 👇
int get total => victories + defeats + draws;
double get victoryPercentage => total > 0 ? victories / total : 0;
@@ -23,6 +22,5 @@ class PieChartData {
'total': total,
'victoryPercentage': victoryPercentage,
'defeatPercentage': defeatPercentage,
'drawPercentage': drawPercentage,
};
}

View File

@@ -1,27 +1,22 @@
import 'package:flutter/material.dart';
import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart';
import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart';
import 'dados_grafico.dart';
import 'controllers/contollers_grafico.dart';
import 'package:playmaker/classe/home.config.dart';
class PieChartCard extends StatefulWidget {
final int victories;
final int defeats;
final int draws;
final PieChartController? controller;
final String title;
final String subtitle;
final Color backgroundColor;
final VoidCallback? onTap;
final double sf;
const PieChartCard({
super.key,
this.victories = 0,
this.defeats = 0,
this.draws = 0,
this.controller,
this.title = 'DESEMPENHO',
this.subtitle = 'Temporada',
this.onTap,
required this.backgroundColor,
this.sf = 1.0,
this.subtitle = 'Vitórias vs Derrotas',
this.onTap, required this.backgroundColor,
});
@override
@@ -29,26 +24,30 @@ class PieChartCard extends StatefulWidget {
}
class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderStateMixin {
late PieChartController _controller;
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(duration: const Duration(milliseconds: 600), vsync: this);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeOutBack));
_controller = widget.controller ?? PieChartController();
_animationController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutBack,
),
);
_animationController.forward();
}
@override
void didUpdateWidget(PieChartCard oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.victories != widget.victories || oldWidget.defeats != widget.defeats || oldWidget.draws != widget.draws) {
_animationController.reset();
_animationController.forward();
}
}
@override
void dispose() {
_animationController.dispose();
@@ -57,160 +56,349 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
@override
Widget build(BuildContext context) {
final data = PieChartData(victories: widget.victories, defeats: widget.defeats, draws: widget.draws);
final data = _controller.chartData;
return AnimatedBuilder(
return AnimatedBuilder(
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),
scale: 0.95 + (_animation.value * 0.05),
child: Opacity(
// 👇 AQUI ESTÁ A FIX: Garante que fica entre 0 e 1
opacity: _animation.value.clamp(0.0, 1.0),
opacity: _animation.value,
child: child,
),
);
},
child: Card(
margin: EdgeInsets.zero,
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
child: InkWell(
onTap: widget.onTap,
borderRadius: BorderRadius.circular(14),
child: Container(
decoration: BoxDecoration(
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) {
final double ch = constraints.maxHeight;
final double cw = constraints.maxWidth;
return Padding(
padding: EdgeInsets.all(cw * 0.06),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 👇 TÍTULOS UM POUCO MAIS PRESENTES
FittedBox(
fit: BoxFit.scaleDown,
child: Text(widget.title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), letterSpacing: 1.0)),
),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(widget.subtitle, style: TextStyle(fontSize: ch * 0.07, fontWeight: FontWeight.bold, color: Colors.white)),
),
SizedBox(height: ch * 0.03),
// MEIO (GRÁFICO + ESTATÍSTICAS)
Expanded(
child: Row(
child: Container(
width: HomeConfig.cardwidthPadding,
height: HomeConfig.cardheightPadding,
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: InkWell(
onTap: widget.onTap,
borderRadius: BorderRadius.circular(20),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
widget.backgroundColor.withOpacity(0.9),
widget.backgroundColor.withOpacity(0.7),
],
),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Cabeçalho compacto
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 1,
child: PieChartWidget(
victoryPercentage: data.victoryPercentage,
defeatPercentage: data.defeatPercentage,
drawPercentage: data.drawPercentage,
sf: widget.sf,
Text(
widget.title,
style: TextStyle(
fontSize: 14, // Pequeno
fontWeight: FontWeight.bold,
color: Colors.white.withOpacity(0.9),
letterSpacing: 1.5,
),
),
SizedBox(width: cw * 0.05),
Expanded(
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_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),
],
SizedBox(height: 2), // Muito pequeno
Text(
widget.subtitle,
style: TextStyle(
fontSize: 16, // Moderado
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
// 👇 RODAPÉ AJUSTADO
SizedBox(height: ch * 0.03),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: ch * 0.035),
decoration: BoxDecoration(
color: Colors.white24, // Igual ao fundo do botão detalhes
borderRadius: BorderRadius.circular(ch * 0.03), // Borda arredondada
Container(
padding: EdgeInsets.all(6), // Pequeno
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.8),
shape: BoxShape.circle,
),
child: Icon(
Icons.pie_chart,
size: 18, // Pequeno
color: Colors.white,
),
),
child: Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Row(
],
),
SizedBox(height: 8), // Pequeno espaço
// Conteúdo principal - COMPACTO E CONTROLADO
Expanded(
child: Row(
children: [
// Gráfico de pizza
Expanded(
flex: 3,
child: Center(
child: PieChartWidget(
victoryPercentage: data.victoryPercentage,
defeatPercentage: data.defeatPercentage,
drawPercentage: data.drawPercentage,
size: 130, // Pequeno para caber
),
),
),
SizedBox(width: 8), // Pequeno espaço
// Estatísticas - EXTREMAMENTE COMPACTAS
Expanded(
flex: 2,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
data.victoryPercentage >= 0.5 ? Icons.trending_up : Icons.trending_down,
color: Colors.green,
size: ch * 0.09
// Vitórias - Layout horizontal ultra compacto
Container(
margin: EdgeInsets.only(bottom: 6), // Muito pequeno
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Número
Text(
data.victories.toString(),
style: TextStyle(
fontSize: 26, // Pequeno mas legível
fontWeight: FontWeight.bold,
color: Colors.green,
height: 0.8,
),
),
SizedBox(width: 6), // Pequeno
// Info compacta
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Container(
width: 8,
height: 8,
margin: EdgeInsets.only(right: 4),
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
),
Text(
'VIT',
style: TextStyle(
fontSize: 10, // Muito pequeno
color: Colors.white.withOpacity(0.8),
fontWeight: FontWeight.w600,
),
),
],
),
SizedBox(height: 2),
Text(
'${(data.victoryPercentage * 100).toStringAsFixed(0)}%',
style: TextStyle(
fontSize: 12, // Pequeno
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
),
SizedBox(width: cw * 0.02),
Text(
'WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: ch * 0.05,
fontWeight: FontWeight.bold,
color: Colors.white
)
// Divisor sutil
Container(
height: 0.5,
color: Colors.white.withOpacity(0.1),
margin: EdgeInsets.symmetric(vertical: 4),
),
// Derrotas
Container(
margin: EdgeInsets.only(bottom: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
data.defeats.toString(),
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.red,
height: 0.8,
),
),
SizedBox(width: 6),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Container(
width: 8,
height: 8,
margin: EdgeInsets.only(right: 4),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
Text(
'DER',
style: TextStyle(
fontSize: 10,
color: Colors.white.withOpacity(0.8),
fontWeight: FontWeight.w600,
),
),
],
),
SizedBox(height: 2),
Text(
'${(data.defeatPercentage * 100).toStringAsFixed(0)}%',
style: TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
),
// Divisor sutil
Container(
height: 0.5,
color: Colors.white.withOpacity(0.1),
margin: EdgeInsets.symmetric(vertical: 4),
),
// Total
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
data.total.toString(),
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.white,
height: 0.8,
),
),
SizedBox(width: 6),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Container(
width: 8,
height: 8,
margin: EdgeInsets.only(right: 4),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
Text(
'TOT',
style: TextStyle(
fontSize: 10,
color: Colors.white.withOpacity(0.8),
fontWeight: FontWeight.w600,
),
),
],
),
SizedBox(height: 2),
Text(
'100%',
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
],
),
),
),
],
),
],
),
);
}
),
SizedBox(height: 10), // Espaço controlado
// Win rate - Sempre visível e não sobreposto
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
data.victoryPercentage > 0.5
? Icons.trending_up
: Icons.trending_down,
color: data.victoryPercentage > 0.5
? Colors.green
: Colors.red,
size: 18, // Pequeno
),
SizedBox(width: 8),
Text(
'Win Rate: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 14, // Pequeno
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
],
),
),
),
),
),
),
);
}
// 👇 PERCENTAGENS SUBIDAS LIGEIRAMENTE (0.10 e 0.045)
Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch) {
return Padding(
padding: EdgeInsets.only(bottom: ch * 0.01),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 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))),
]),
),
],
),
);
}
Widget _buildDynDivider(double ch) {
return Container(height: 0.5, color: Colors.white.withOpacity(0.1), margin: EdgeInsets.symmetric(vertical: ch * 0.01));
}
}

View File

@@ -5,70 +5,55 @@ class PieChartWidget extends StatelessWidget {
final double victoryPercentage;
final double defeatPercentage;
final double drawPercentage;
final double sf;
final double size;
const PieChartWidget({
super.key,
required this.victoryPercentage,
required this.defeatPercentage,
this.drawPercentage = 0,
required this.sf,
this.size = 140, // Aumentado para 400x300
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
// 👇 MAGIA ANTI-DESAPARECIMENTO 👇
// Vê o espaço real. Se por algum motivo for infinito, assume 100 para não sumir.
final double w = constraints.maxWidth.isInfinite ? 100.0 : constraints.maxWidth;
final double h = constraints.maxHeight.isInfinite ? 100.0 : constraints.maxHeight;
// Pega no menor valor para garantir que o círculo não é cortado
final double size = math.min(w, h);
return Center(
child: SizedBox(
width: size,
height: size,
child: CustomPaint(
painter: _PieChartPainter(
victoryPercentage: victoryPercentage,
defeatPercentage: defeatPercentage,
drawPercentage: drawPercentage,
),
child: Center(
child: _buildCenterLabels(size),
),
),
),
);
},
return SizedBox(
width: size,
height: size,
child: CustomPaint(
painter: _PieChartPainter(
victoryPercentage: victoryPercentage,
defeatPercentage: defeatPercentage,
drawPercentage: drawPercentage,
),
child: _buildCenterLabels(),
),
);
}
Widget _buildCenterLabels(double size) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${(victoryPercentage * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: size * 0.18, // O texto cresce ou encolhe com o círculo
fontWeight: FontWeight.bold,
color: Colors.white,
Widget _buildCenterLabels() {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${(victoryPercentage * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: size * 0.2, // Tamanho responsivo (28px para 140px)
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
SizedBox(height: size * 0.02),
Text(
'Vitórias',
style: TextStyle(
fontSize: size * 0.10,
color: Colors.white.withOpacity(0.8),
SizedBox(height: 4),
Text(
'Vitórias',
style: TextStyle(
fontSize: size * 0.1, // Tamanho responsivo (14px para 140px)
color: Colors.white.withOpacity(0.8),
),
),
),
],
],
),
);
}
}
@@ -87,53 +72,65 @@ class _PieChartPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
// Margem de 5% para a linha de fora não ser cortada
final radius = (size.width / 2) - (size.width * 0.05);
final radius = size.width / 2 - 5;
// Cores
const victoryColor = Colors.green;
const defeatColor = Colors.red;
const drawColor = Colors.yellow;
const borderColor = Colors.white30;
double startAngle = -math.pi / 2;
double startAngle = 0;
// Vitórias (verde)
if (victoryPercentage > 0) {
final sweepAngle = 2 * math.pi * victoryPercentage;
_drawSector(canvas, center, radius, startAngle, sweepAngle, victoryColor, size.width);
_drawSector(canvas, center, radius, startAngle, sweepAngle, victoryColor);
startAngle += sweepAngle;
}
// Empates (amarelo)
if (drawPercentage > 0) {
final sweepAngle = 2 * math.pi * drawPercentage;
_drawSector(canvas, center, radius, startAngle, sweepAngle, drawColor, size.width);
_drawSector(canvas, center, radius, startAngle, sweepAngle, drawColor);
startAngle += sweepAngle;
}
// Derrotas (vermelho)
if (defeatPercentage > 0) {
final sweepAngle = 2 * math.pi * defeatPercentage;
_drawSector(canvas, center, radius, startAngle, sweepAngle, defeatColor, size.width);
_drawSector(canvas, center, radius, startAngle, sweepAngle, defeatColor);
}
// Borda
final borderPaint = Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = size.width * 0.02;
..strokeWidth = 2;
canvas.drawCircle(center, radius, borderPaint);
}
void _drawSector(Canvas canvas, Offset center, double radius, double startAngle, double sweepAngle, Color color, double totalWidth) {
void _drawSector(Canvas canvas, Offset center, double radius,
double startAngle, double sweepAngle, Color color) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, true, paint);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
true,
paint,
);
// Linha divisória
if (sweepAngle < 2 * math.pi) {
final linePaint = Paint()
..color = Colors.white.withOpacity(0.5)
..style = PaintingStyle.stroke
..strokeWidth = totalWidth * 0.015;
..strokeWidth = 1.5;
final lineX = center.dx + radius * math.cos(startAngle);
final lineY = center.dy + radius * math.sin(startAngle);

View File

@@ -2,6 +2,8 @@ class Game {
final String id;
final String myTeam;
final String opponentTeam;
final String? myTeamLogo; // URL da imagem
final String? opponentTeamLogo; // URL da imagem
final String myScore;
final String opponentScore;
final String status;
@@ -11,22 +13,26 @@ class Game {
required this.id,
required this.myTeam,
required this.opponentTeam,
this.myTeamLogo,
this.opponentTeamLogo,
required this.myScore,
required this.opponentScore,
required this.status,
required this.season,
});
// No seu factory, certifique-se de mapear os campos da tabela (ou de um JOIN)
factory Game.fromMap(Map<String, dynamic> map) {
return Game(
// O "?." converte para texto com segurança, e o "?? '...'" diz o que mostrar se for nulo (vazio)
id: map['id']?.toString() ?? '',
myTeam: map['my_team']?.toString() ?? 'Desconhecida',
opponentTeam: map['opponent_team']?.toString() ?? 'Adversário',
myScore: map['my_score']?.toString() ?? '0',
opponentScore: map['opponent_score']?.toString() ?? '0',
status: map['status']?.toString() ?? 'Terminado',
season: map['season']?.toString() ?? 'Sem Época',
id: map['id'],
myTeam: map['my_team_name'],
opponentTeam: map['opponent_team_name'],
myTeamLogo: map['my_team_logo'], // Certifique-se que o Supabase retorna isto
opponentTeamLogo: map['opponent_team_logo'],
myScore: map['my_score'].toString(),
opponentScore: map['opponent_score'].toString(),
status: map['status'],
season: map['season'],
);
}
}

View File

@@ -1,325 +1,265 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:playmaker/controllers/placar_controller.dart';
import 'package:playmaker/utils/size_extension.dart';
import 'package:playmaker/widgets/placar_widgets.dart';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:playmaker/controllers/placar_controller.dart';
import 'package:playmaker/widgets/placar_widgets.dart';
import 'dart:math' as math;
class PlacarPage extends StatefulWidget {
final String gameId, myTeam, opponentTeam;
const PlacarPage({super.key, required this.gameId, required this.myTeam, required this.opponentTeam});
class PlacarPage extends StatefulWidget {
final String gameId, myTeam, opponentTeam;
const PlacarPage({super.key, required this.gameId, required this.myTeam, required this.opponentTeam});
@override
State<PlacarPage> createState() => _PlacarPageState();
@override
State<PlacarPage> createState() => _PlacarPageState();
}
class _PlacarPageState extends State<PlacarPage> {
late PlacarController _controller;
@override
void initState() {
super.initState();
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeLeft,
]);
_controller = PlacarController(
gameId: widget.gameId,
myTeam: widget.myTeam,
opponentTeam: widget.opponentTeam,
onUpdate: () {
if (mounted) setState(() {});
}
);
_controller.loadPlayers();
}
class _PlacarPageState extends State<PlacarPage> {
late PlacarController _controller;
@override
void dispose() {
_controller.dispose();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
super.dispose();
}
@override
void initState() {
super.initState();
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeLeft,
]);
_controller = PlacarController(
gameId: widget.gameId,
myTeam: widget.myTeam,
opponentTeam: widget.opponentTeam,
onUpdate: () {
if (mounted) setState(() {});
}
);
_controller.loadPlayers();
}
@override
void dispose() {
_controller.dispose();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
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,
child: Draggable<String>(
data: action,
feedback: Material(
color: Colors.transparent,
child: CircleAvatar(
radius: 30 * sf,
backgroundColor: color.withOpacity(0.8),
child: Icon(icon, color: Colors.white, size: 30 * sf)
),
),
child: Column(
children: [
CircleAvatar(
radius: 27 * sf,
backgroundColor: color,
child: Icon(icon, color: Colors.white, size: 28 * sf),
),
SizedBox(height: 5 * sf),
Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 * sf)),
],
Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top, double h) {
return Positioned(
top: top,
left: left > 0 ? left : null,
right: right > 0 ? right : null,
child: Draggable<String>(
data: action,
feedback: Material(
color: Colors.transparent,
child: CircleAvatar(
radius: 30,
backgroundColor: color.withOpacity(0.8),
child: Icon(icon, color: Colors.white)
),
),
);
}
// --- BOTÕES LATERAIS QUADRADOS ---
Widget _buildCornerBtn({required String heroTag, required IconData icon, required Color color, required VoidCallback onTap, required double size, bool isLoading = false}) {
return SizedBox(
width: size,
height: size,
child: FloatingActionButton(
heroTag: heroTag,
backgroundColor: color,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * (size / 50))),
elevation: 5,
onPressed: isLoading ? null : onTap,
child: isLoading
? SizedBox(width: size*0.45, height: size*0.45, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5))
: Icon(icon, color: Colors.white, size: size * 0.55),
),
);
}
@override
Widget build(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
// 👇 CÁLCULO MANUAL DO SF 👇
final double sf = math.min(wScreen / 1150, hScreen / 720);
final double cornerBtnSize = 48 * sf; // Tamanho ideal (Nem 38 nem 55)
if (_controller.isLoading) {
return Scaffold(
backgroundColor: const Color(0xFF16202C),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("PREPARANDO O PAVILHÃO", style: TextStyle(color: Colors.white24, fontSize: 45 * sf, fontWeight: FontWeight.bold, letterSpacing: 2)),
SizedBox(height: 35 * sf),
StreamBuilder(
stream: Stream.periodic(const Duration(seconds: 3)),
builder: (context, snapshot) {
List<String> frases = [
"O Treinador está a desenhar a tática...",
"A encher as bolas com ar de campeão...",
"O árbitro está a testar o apito...",
"A verificar se o cesto está nivelado...",
"Os jogadores estão a terminar o aquecimento..."
];
String frase = frases[DateTime.now().second % frases.length];
return Text(frase, style: TextStyle(color: Colors.orange.withOpacity(0.7), fontSize: 26 * sf, fontStyle: FontStyle.italic));
},
),
],
child: Column(
children: [
CircleAvatar(
radius: 25,
backgroundColor: color,
child: Icon(icon, color: Colors.white, size: 30),
),
),
);
}
Text(label, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12)),
],
),
),
);
}
@override
Widget build(BuildContext context) {
if (_controller.isLoading) {
return Scaffold(
backgroundColor: const Color(0xFF266174),
body: SafeArea(
top: false,
bottom: false,
// 👇 A MÁGICA DO IGNORE POINTER COMEÇA AQUI 👇
child: IgnorePointer(
ignoring: _controller.isSaving, // Se estiver a gravar, ignora os toques!
child: Stack(
children: [
// --- O CAMPO ---
Container(
margin: EdgeInsets.only(left: 65 * sf, right: 65 * sf, bottom: 55 * sf),
decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.5)),
child: LayoutBuilder(
builder: (context, constraints) {
final w = constraints.maxWidth;
final h = constraints.maxHeight;
return Stack(
children: [
GestureDetector(
onTapDown: (details) {
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(
// Agora usamos relativeX e relativeY multiplicados pela largura(w) e altura(h)
left: (shot.relativeX * w) - (9 * context.sf),
top: (shot.relativeY * h) - (9 * context.sf),
child: CircleAvatar(
radius: 9 * context.sf,
backgroundColor: shot.isMake ? Colors.green : Colors.red,
child: Icon(shot.isMake ? Icons.check : Icons.close, size: 11 * context.sf, color: Colors.white)
),
)).toList(),
),
),
),
// --- JOGADORES ---
if (!_controller.isSelectingShotLocation) ...[
Positioned(top: h * 0.25, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[0], isOpponent: false, sf: sf)),
Positioned(top: h * 0.68, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[1], isOpponent: false, sf: sf)),
Positioned(top: h * 0.45, left: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[2], isOpponent: false, sf: sf)),
Positioned(top: h * 0.15, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[3], isOpponent: false, sf: sf)),
Positioned(top: h * 0.80, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[4], isOpponent: false, sf: sf)),
Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[0], isOpponent: true, sf: sf)),
Positioned(top: h * 0.68, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[1], isOpponent: true, sf: sf)),
Positioned(top: h * 0.45, right: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[2], isOpponent: true, sf: sf)),
Positioned(top: h * 0.15, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[3], isOpponent: true, sf: sf)),
Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[4], isOpponent: true, sf: sf)),
],
// --- BOTÕES DE FALTAS ---
if (!_controller.isSelectingShotLocation) ...[
_buildFloatingFoulBtn("FALTA +", Colors.orange, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31, sf),
_buildFloatingFoulBtn("FALTA -", Colors.redAccent, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31, sf),
],
// --- BOTÃO PLAY/PAUSE ---
if (!_controller.isSelectingShotLocation)
Positioned(
top: (h * 0.32) + (40 * sf),
left: 0, right: 0,
child: Center(
child: GestureDetector(
onTap: () => _controller.toggleTimer(context),
child: CircleAvatar(
radius: 68 * sf,
backgroundColor: Colors.grey.withOpacity(0.5),
child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 58 * sf)
),
),
),
),
// --- PLACAR NO TOPO ---
Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))),
// --- BOTÕES DE AÇÃO ---
if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller, sf: sf)),
// --- OVERLAY LANÇAMENTO ---
if (_controller.isSelectingShotLocation)
Positioned(
top: h * 0.4, left: 0, right: 0,
child: Center(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 35 * sf, vertical: 18 * sf),
decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(11 * sf), border: Border.all(color: Colors.white, width: 1.5 * sf)),
child: Text("TOQUE NO CAMPO PARA MARCAR O LOCAL DO LANÇAMENTO", style: TextStyle(color: Colors.white, fontSize: 27 * sf, fontWeight: FontWeight.bold)),
),
),
),
],
backgroundColor: const Color(0xFF16202C),
body: Stack(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"PREPARANDO O PAVILHÃO",
style: TextStyle(color: Colors.white24, fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 2)
),
const SizedBox(height: 15),
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: 18, fontStyle: FontStyle.italic)
);
},
),
),
// --- 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(
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('🛑 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),
),
),
],
],
),
),
),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 1500),
builder: (context, value, child) {
double bounce = (math.sin(value * math.pi * 4)).abs();
return AnimatedAlign(
duration: const Duration(milliseconds: 1500),
alignment: Alignment(value > 0.5 ? 0.9 : -0.9, 0.7 - (bounce * 0.4)),
child: const Icon(Icons.sports_basketball, size: 70, color: Colors.orange),
);
},
onEnd: () => setState(() {}),
),
],
),
);
}
}
return Scaffold(
backgroundColor: const Color(0xFF266174),
body: Stack(
children: [
Container(
margin: const EdgeInsets.only(left: 60, right: 60, bottom: 50),
decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.0)),
child: LayoutBuilder(
builder: (context, constraints) {
final w = constraints.maxWidth;
final h = constraints.maxHeight;
return Stack(
children: [
// --- MAPA DO CAMPO ---
GestureDetector(
onTapDown: (details) {
if (_controller.isSelectingShotLocation) {
// AQUI É QUE A MAGIA ACONTECE! Passamos o tamanho exato do layout
_controller.registerShotLocation(
context,
details.localPosition,
Size(w, h)
);
}
},
child: Container(
decoration: const BoxDecoration(
image: DecorationImage(image: AssetImage('assets/campo.png'), fit: BoxFit.cover, alignment: Alignment(0.0, 0.2)),
),
child: Stack(
children: _controller.matchShots.map((shot) => Positioned(
left: shot.position.dx - 8, top: shot.position.dy - 8,
child: CircleAvatar(radius: 8, backgroundColor: shot.isMake ? Colors.green : Colors.red, child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white)),
)).toList(),
),
),
),
// --- JOGADORES EM CAMPO ---
if (!_controller.isSelectingShotLocation) ...[
Positioned(top: h * 0.25, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[0], isOpponent: false)),
Positioned(top: h * 0.68, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[1], isOpponent: false)),
Positioned(top: h * 0.45, left: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[2], isOpponent: false)),
Positioned(top: h * 0.15, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[3], isOpponent: false)),
Positioned(top: h * 0.80, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[4], isOpponent: false)),
Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[0], isOpponent: true)),
Positioned(top: h * 0.68, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[1], isOpponent: true)),
Positioned(top: h * 0.45, right: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[2], isOpponent: true)),
Positioned(top: h * 0.15, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[3], isOpponent: true)),
Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[4], isOpponent: true)),
],
// --- BOTÕES DE FALTA (FLUTUANTES) ---
if (!_controller.isSelectingShotLocation) ...[
_buildFloatingFoulBtn("FALTA +", Colors.orange, "add_foul", Icons.sports, w * 0.38, 0, h * 0.30, h),
_buildFloatingFoulBtn("FALTA -", Colors.redAccent, "sub_foul", Icons.block, 0, w * 0.38, h * 0.30, h),
],
// --- BOTÃO CENTRAL DO TEMPO ---
if (!_controller.isSelectingShotLocation)
Positioned(
top: (h * 0.30) + 70, left: 0, right: 0,
child: Center(
child: GestureDetector(
onTap: () => _controller.toggleTimer(context),
child: CircleAvatar(radius: 60, backgroundColor: Colors.grey.withOpacity(0.5), child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 50)),
),
),
),
// --- PLACAR E BOTÕES DE AÇÃO ---
Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller))),
if (!_controller.isSelectingShotLocation) Positioned(bottom: 10, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller)),
// --- OVERLAY DE MARCAÇÃO DE LANÇAMENTO ---
if (_controller.isSelectingShotLocation)
Positioned(
top: h * 0.4, left: 0, right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.white)),
child: const Text("TOQUE NO CAMPO PARA MARCAR O LOCAL DO LANÇAMENTO", style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
),
),
),
],
);
},
),
),
// --- MENUS LATERAIS E BANCOS DE SUPLENTES ---
if (!_controller.isSelectingShotLocation) ...[
Positioned(
top: 20, left: 10,
child: FloatingActionButton(
heroTag: 'btn_save',
backgroundColor: const Color(0xFF16202C),
mini: true,
onPressed: _controller.isSaving ? null : () => _controller.saveGameStats(context),
child: _controller.isSaving
? const SizedBox(width: 15, height: 15, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: const Icon(Icons.save, color: Colors.white)
)
),
Positioned(top: 70, left: 10, child: FloatingActionButton(heroTag: 'btn_exit', backgroundColor: const Color(0xFFD92C2C), mini: true, onPressed: () => Navigator.pop(context), child: const Icon(Icons.exit_to_app, color: Colors.white))),
Positioned(
bottom: 50, left: 10,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_controller.showMyBench) BenchPlayersList(controller: _controller, isOpponent: false),
const SizedBox(height: 10),
FloatingActionButton(heroTag: 'btn_sub_home', backgroundColor: const Color(0xFF1E5BB2), mini: true, onPressed: () { _controller.showMyBench = !_controller.showMyBench; _controller.onUpdate(); }, child: const Icon(Icons.swap_horiz, color: Colors.white)),
],
),
),
Positioned(
bottom: 50, right: 10,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_controller.showOppBench) BenchPlayersList(controller: _controller, isOpponent: true),
const SizedBox(height: 10),
FloatingActionButton(heroTag: 'btn_sub_away', backgroundColor: const Color(0xFFD92C2C), mini: true, onPressed: () { _controller.showOppBench = !_controller.showOppBench; _controller.onUpdate(); }, child: const Icon(Icons.swap_horiz, color: Colors.white)),
],
),
),
],
],
),
);
}
}

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import '../controllers/register_controller.dart';
import '../widgets/register_widgets.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
class RegisterPage extends StatefulWidget {
const RegisterPage({super.key});
@@ -11,44 +10,39 @@ class RegisterPage extends StatefulWidget {
}
class _RegisterPageState extends State<RegisterPage> {
// Instancia o controller
final RegisterController _controller = RegisterController();
@override
void dispose() {
_controller.dispose();
_controller.dispose(); // Limpa a memória ao sair
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text("Criar Conta", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
backgroundColor: Colors.white,
elevation: 0,
),
appBar: AppBar(title: const Text("Criar Conta")),
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.all(24.0 * context.sf),
padding: const EdgeInsets.all(24.0),
child: ListenableBuilder(
listenable: _controller,
listenable: _controller, // Ouve as mudanças (loading)
builder: (context, child) {
return Container(
width: double.infinity,
constraints: BoxConstraints(maxWidth: 450 * context.sf), // Mesma largura do Login
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const RegisterHeader(), // 🔥 Agora sim, usa o Header bonito!
SizedBox(height: 30 * context.sf),
RegisterFormFields(controller: _controller),
SizedBox(height: 24 * context.sf),
RegisterButton(controller: _controller),
],
),
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Junta-te à Equipa!",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 30),
// Widgets Extraídos
RegisterFormFields(controller: _controller),
const SizedBox(height: 24),
RegisterButton(controller: _controller),
],
);
},
),

View File

@@ -1,196 +1,9 @@
import 'package:flutter/material.dart';
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'; // 👇 NOVO SUPERPODER AQUI TAMBÉM!
import '../widgets/game_widgets.dart';
// --- CARD DE EXIBIÇÃO DO JOGO ---
class GameResultCard extends StatelessWidget {
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
final String? myTeamLogo, opponentTeamLogo;
const GameResultCard({
super.key, required this.gameId, required this.myTeam, required this.opponentTeam,
required this.myScore, required this.opponentScore, required this.status, required this.season,
this.myTeamLogo, this.opponentTeamLogo,
});
@override
Widget build(BuildContext context) {
return Container(
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(context, myTeam, const Color(0xFFE74C3C), myTeamLogo)),
_buildScoreCenter(context, gameId),
Expanded(child: _buildTeamInfo(context, opponentTeam, Colors.black87, opponentTeamLogo)),
],
),
);
}
Widget _buildTeamInfo(BuildContext context, String name, Color color, String? logoUrl) {
return Column(
children: [
CircleAvatar(radius: 24 * context.sf, backgroundColor: color, backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * context.sf) : null),
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) {
return Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
_scoreBox(context, myScore, Colors.green),
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * context.sf)),
_scoreBox(context, opponentScore, Colors.grey),
],
),
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 * 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 * context.sf),
Text(status, style: TextStyle(fontSize: 12 * context.sf, color: Colors.blue, fontWeight: FontWeight.bold)),
],
);
}
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)),
);
}
// --- POPUP DE CRIAÇÃO ---
class CreateGameDialogManual extends StatefulWidget {
final TeamController teamController;
final GameController gameController;
const CreateGameDialogManual({super.key, required this.teamController, required this.gameController});
@override
State<CreateGameDialogManual> createState() => _CreateGameDialogManualState();
}
class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
late TextEditingController _seasonController;
final TextEditingController _myTeamController = TextEditingController();
final TextEditingController _opponentController = TextEditingController();
bool _isLoading = false;
@override
void initState() {
super.initState();
_seasonController = TextEditingController(text: _calculateSeason());
}
String _calculateSeason() {
final now = DateTime.now();
return now.month >= 7 ? "${now.year}/${(now.year + 1).toString().substring(2)}" : "${now.year - 1}/${now.year.toString().substring(2)}";
}
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
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 * context.sf))),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)),
onPressed: _isLoading ? null : () async {
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
setState(() => _isLoading = true);
String? newGameId = await widget.gameController.createGame(_myTeamController.text, _opponentController.text, _seasonController.text);
setState(() => _isLoading = false);
if (newGameId != null && context.mounted) {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: newGameId, myTeam: _myTeamController.text, opponentTeam: _opponentController.text)));
}
}
},
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(BuildContext context, String label, TextEditingController controller) {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: widget.teamController.teamsStream,
builder: (context, snapshot) {
List<Map<String, dynamic>> teamList = snapshot.hasData ? snapshot.data! : [];
return Autocomplete<Map<String, dynamic>>(
displayStringForOption: (Map<String, dynamic> option) => option['name'].toString(),
optionsBuilder: (TextEditingValue val) {
if (val.text.isEmpty) return const Iterable<Map<String, dynamic>>.empty();
return teamList.where((t) => t['name'].toString().toLowerCase().contains(val.text.toLowerCase()));
},
onSelected: (Map<String, dynamic> selection) { controller.text = selection['name'].toString(); },
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0, borderRadius: BorderRadius.circular(8 * context.sf),
child: ConstrainedBox(
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) {
final option = options.elementAt(index);
final String name = option['name'].toString();
final String? imageUrl = option['image_url'];
return ListTile(
leading: CircleAvatar(radius: 20 * context.sf, backgroundColor: Colors.grey.shade200, backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * context.sf) : null),
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * context.sf)),
onTap: () { onSelected(option); },
);
},
),
),
),
);
},
fieldViewBuilder: (ctx, txtCtrl, node, submit) {
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 * context.sf),
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * context.sf), prefixIcon: Icon(Icons.search, size: 20 * context.sf), border: const OutlineInputBorder()),
);
},
);
},
);
}
}
// --- PÁGINA PRINCIPAL DOS JOGOS ---
class GamePage extends StatefulWidget {
const GamePage({super.key});
@@ -201,54 +14,68 @@ class GamePage extends StatefulWidget {
class _GamePageState extends State<GamePage> {
final GameController gameController = GameController();
final TeamController teamController = TeamController();
String selectedSeason = 'Todas';
String selectedTeam = 'Todas';
@override
Widget build(BuildContext context) {
bool isFilterActive = selectedSeason != 'Todas' || selectedTeam != 'Todas';
return Scaffold(
backgroundColor: const Color(0xFFF5F7FA),
appBar: AppBar(
title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)),
title: const Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: Colors.white,
elevation: 0,
actions: [
Padding(
padding: EdgeInsets.only(right: 8.0 * context.sf),
child: IconButton(
icon: Icon(isFilterActive ? Icons.filter_list_alt : Icons.filter_list, color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87, size: 26 * context.sf),
onPressed: () => _showFilterPopup(context),
),
)
],
),
// 1º STREAM: Lemos as equipas para ter as imagens
body: StreamBuilder<List<Map<String, dynamic>>>(
stream: teamController.teamsStream,
builder: (context, teamSnapshot) {
final List<Map<String, dynamic>> teamsList = teamSnapshot.data ?? [];
// 2º STREAM: Lemos os jogos
return StreamBuilder<List<Game>>(
stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason),
stream: gameController.gamesStream,
builder: (context, gameSnapshot) {
if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) return const Center(child: CircularProgressIndicator());
if (gameSnapshot.hasError) return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * context.sf)));
if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) {
return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.search_off, size: 48 * context.sf, color: Colors.grey.shade300), SizedBox(height: 10 * context.sf), Text("Nenhum jogo encontrado.", style: TextStyle(fontSize: 14 * context.sf, color: Colors.grey.shade600))]));
if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (gameSnapshot.hasError) {
return Center(child: Text("Erro: ${gameSnapshot.error}"));
}
if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) {
return const Center(child: Text("Nenhum jogo registado."));
}
return ListView.builder(
padding: EdgeInsets.all(16 * context.sf),
padding: const EdgeInsets.all(16),
itemCount: gameSnapshot.data!.length,
itemBuilder: (context, index) {
final game = gameSnapshot.data![index];
String? myLogo, oppLogo;
// --- LÓGICA PARA ENCONTRAR A IMAGEM PELO NOME ---
String? myLogo;
String? oppLogo;
for (var team in teamsList) {
if (team['name'] == game.myTeam) myLogo = team['image_url'];
if (team['name'] == game.opponentTeam) oppLogo = team['image_url'];
if (team['name'] == game.myTeam) {
myLogo = team['image_url'];
}
if (team['name'] == game.opponentTeam) {
oppLogo = team['image_url'];
}
}
// Agora já passamos as imagens para o cartão!
return GameResultCard(
gameId: game.id, myTeam: game.myTeam, opponentTeam: game.opponentTeam, myScore: game.myScore,
opponentScore: game.opponentScore, status: game.status, season: game.season, myTeamLogo: myLogo, opponentTeamLogo: oppLogo,
gameId: game.id,
myTeam: game.myTeam,
opponentTeam: game.opponentTeam,
myScore: game.myScore,
opponentScore: game.opponentScore,
status: game.status,
season: game.season,
myTeamLogo: myLogo, // <-- IMAGEM DA TUA EQUIPA
opponentTeamLogo: oppLogo, // <-- IMAGEM DA EQUIPA ADVERSÁRIA
);
},
);
@@ -257,77 +84,20 @@ class _GamePageState extends State<GamePage> {
},
),
floatingActionButton: FloatingActionButton(
heroTag: 'add_game_btn', // 👇 A MÁGICA ESTÁ AQUI TAMBÉM!
backgroundColor: const Color(0xFFE74C3C),
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
onPressed: () => showDialog(context: context, builder: (context) => CreateGameDialogManual(teamController: teamController, gameController: gameController)),
child: const Icon(Icons.add, color: Colors.white),
onPressed: () => _showCreateDialog(context),
),
);
}
void _showFilterPopup(BuildContext context) {
String tempSeason = selectedSeason;
String tempTeam = selectedTeam;
void _showCreateDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setPopupState) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)),
IconButton(icon: const Icon(Icons.close, color: Colors.grey), onPressed: () => Navigator.pop(context), padding: EdgeInsets.zero, constraints: const BoxConstraints())
],
),
content: Column(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Temporada", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * context.sf),
Container(
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true, value: tempSeason, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold),
items: ['Todas', '2024/25', '2025/26'].map((String value) => DropdownMenuItem<String>(value: value, child: Text(value))).toList(),
onChanged: (newValue) => setPopupState(() => tempSeason = newValue!),
),
),
),
SizedBox(height: 20 * context.sf),
Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * context.sf),
Container(
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)),
child: StreamBuilder<List<Map<String, dynamic>>>(
stream: teamController.teamsStream,
builder: (context, snapshot) {
List<String> teamNames = ['Todas'];
if (snapshot.hasData) teamNames.addAll(snapshot.data!.map((t) => t['name'].toString()));
if (!teamNames.contains(tempTeam)) tempTeam = 'Todas';
return DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true, value: tempTeam, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold),
items: teamNames.map((String value) => DropdownMenuItem<String>(value: value, child: Text(value, overflow: TextOverflow.ellipsis))).toList(),
onChanged: (newValue) => setPopupState(() => tempTeam = newValue!),
),
);
}
),
),
],
),
actions: [
TextButton(onPressed: () { setState(() { selectedSeason = 'Todas'; selectedTeam = 'Todas'; }); Navigator.pop(context); }, child: Text('LIMPAR', style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey))),
ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), onPressed: () { setState(() { selectedSeason = tempSeason; selectedTeam = tempTeam; }); Navigator.pop(context); }, child: Text('APLICAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * context.sf))),
],
);
}
);
},
builder: (context) => CreateGameDialogManual(
teamController: teamController,
gameController: gameController,
),
);
}
}

View File

@@ -1,13 +1,8 @@
import 'package:flutter/material.dart';
import 'package:playmaker/classe/home.config.dart';
import 'package:playmaker/grafico%20de%20pizza/grafico.dart';
import 'package:playmaker/grafico%20de%20pizza/grafico.dart';
import 'package:playmaker/pages/gamePage.dart';
import 'package:playmaker/pages/teamPage.dart';
import 'package:playmaker/controllers/team_controller.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:playmaker/pages/status_page.dart';
import '../utils/size_extension.dart';
import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@@ -18,414 +13,214 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
final TeamController _teamController = TeamController();
String? _selectedTeamId;
String _selectedTeamName = "Selecionar Equipa";
int _teamWins = 0;
int _teamLosses = 0;
int _teamDraws = 0;
final _supabase = Supabase.instance.client;
late final List<Widget> _pages;
@override
void initState() {
super.initState();
_pages = [
_buildHomeContent(),
const GamePage(),
const TeamsPage(),
const Center(child: Text('Tela de Status')),
];
}
void _onItemSelected(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
// Já não precisamos calcular o sf aqui!
final List<Widget> pages = [
_buildHomeContent(context), // Passamos só o context
const GamePage(),
const TeamsPage(),
const StatusPage(),
];
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)),
title: const Text('PlayMaker'),
backgroundColor: HomeConfig.primaryColor,
foregroundColor: Colors.white,
leading: IconButton(
icon: Icon(Icons.person, size: 24 * context.sf),
icon: const Icon(Icons.person),
onPressed: () {},
),
),
body: IndexedStack(
index: _selectedIndex,
children: pages,
children: _pages,
),
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) => setState(() => _selectedIndex = index),
onDestinationSelected: _onItemSelected,
backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
elevation: 1,
// 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),
height: 70,
destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'),
NavigationDestination(icon: Icon(Icons.sports_soccer_outlined), selectedIcon: Icon(Icons.sports_soccer), label: 'Jogo'),
NavigationDestination(icon: Icon(Icons.people_outline), selectedIcon: Icon(Icons.people), label: 'Equipas'),
NavigationDestination(icon: Icon(Icons.insights_outlined), selectedIcon: Icon(Icons.insights), label: 'Status'),
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home_filled),
label: 'Home',
),
NavigationDestination(
icon: Icon(Icons.sports_soccer_outlined),
selectedIcon: Icon(Icons.sports_soccer),
label: 'Jogo',
),
NavigationDestination(
icon: Icon(Icons.people_outline),
selectedIcon: Icon(Icons.people),
label: 'Equipas',
),
NavigationDestination(
icon: Icon(Icons.insights_outlined),
selectedIcon: Icon(Icons.insights),
label: 'Status',
),
],
),
);
}
void _showTeamSelector(BuildContext context) {
showModalBottomSheet(
context: context,
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 * context.sf, child: const Center(child: Text("Nenhuma equipa criada.")));
final teams = snapshot.data!;
return ListView.builder(
shrinkWrap: true,
itemCount: teams.length,
itemBuilder: (context, index) {
final team = teams[index];
return ListTile(
title: Text(team['name']),
onTap: () {
setState(() {
_selectedTeamId = team['id'];
_selectedTeamName = team['name'];
_teamWins = team['wins'] != null ? int.tryParse(team['wins'].toString()) ?? 0 : 0;
_teamLosses = team['losses'] != null ? int.tryParse(team['losses'].toString()) ?? 0 : 0;
_teamDraws = team['draws'] != null ? int.tryParse(team['draws'].toString()) ?? 0 : 0;
});
Navigator.pop(context);
},
);
},
);
},
);
},
Widget _buildHomeContent() {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildStatCard(
title: 'Mais Pontos',
playerName: 'Michael Jordan',
statValue: '34.5',
statLabel: 'PPG',
color: Colors.blue[800]!,
icon: Icons.sports_basketball,
isHighlighted: true,
),
const SizedBox(width: 20),
_buildStatCard(
title: 'Mais Assistências',
playerName: 'Magic Johnson',
statValue: '12.8',
statLabel: 'APG',
color: Colors.green[800]!,
icon: Icons.sports_basketball,
isHighlighted: false,
),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildStatCard(
title: 'Mais Rebotes',
playerName: 'Dennis Rodman',
statValue: '15.3',
statLabel: 'RPG',
color: Colors.purple[800]!,
icon: Icons.sports_basketball,
isHighlighted: false,
),
const SizedBox(width: 20),
PieChartCard(
title: 'DESEMPENHO',
subtitle: 'Vitórias vs Derrotas',
backgroundColor: Colors.red[800]!,
onTap: () {},
),
],
),
const SizedBox(height: 40),
Text(
'Histórico de Jogos',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
],
),
),
);
}
Widget _buildHomeContent(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double cardHeight = wScreen * 0.5;
return StreamBuilder<List<Map<String, dynamic>>>(
stream: _selectedTeamId != null
? _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!)
: const Stream.empty(),
builder: (context, snapshot) {
Map<String, dynamic> leaders = _calculateLeaders(snapshot.data ?? []);
return SingleChildScrollView(
Widget _buildStatCard({
required String title,
required String playerName,
required String statValue,
required String statLabel,
required Color color,
required IconData icon,
bool isHighlighted = false,
}) {
return SizedBox(
width: HomeConfig.cardwidthPadding,
height: HomeConfig.cardheightPadding,
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: isHighlighted
? const BorderSide(color: Colors.amber, width: 2)
: BorderSide.none,
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [color.withOpacity(0.9), color.withOpacity(0.7)],
),
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 22.0 * context.sf, vertical: 16.0 * context.sf),
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () => _showTeamSelector(context),
child: Container(
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 * 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 * 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 * 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 * 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 * context.sf),
Expanded(
child: PieChartCard(
victories: _teamWins,
defeats: _teamLosses,
draws: _teamDraws,
title: 'DESEMPENHO',
subtitle: 'Temporada',
backgroundColor: const Color(0xFFC62828),
sf: context.sf // Aqui o PieChartCard ainda usa sf, então passamos
),
),
],
),
),
SizedBox(height: 40 * context.sf),
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 * context.sf),
alignment: Alignment.center,
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'])
.order('game_date', ascending: false),
builder: (context, gameSnapshot) {
if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
if (gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
final todosOsJogos = gameSnapshot.data ?? [];
final gamesList = todosOsJogos.where((game) {
String myT = game['my_team']?.toString() ?? '';
String oppT = game['opponent_team']?.toString() ?? '';
String status = game['status']?.toString() ?? '';
return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado';
}).take(3).toList();
if (gamesList.isEmpty) {
return Container(
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)),
);
}
return Column(
children: gamesList.map((game) {
String dbMyTeam = game['my_team']?.toString() ?? '';
String dbOppTeam = game['opponent_team']?.toString() ?? '';
int dbMyScore = int.tryParse(game['my_score'].toString()) ?? 0;
int dbOppScore = int.tryParse(game['opponent_score'].toString()) ?? 0;
String opponent; int myScore; int oppScore;
if (dbMyTeam == _selectedTeamName) {
opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore;
} else {
opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore;
}
String rawDate = game['game_date']?.toString() ?? '---';
String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
String result = 'E';
if (myScore > oppScore) result = 'V';
if (myScore < oppScore) result = 'D';
return _buildGameHistoryCard(
context: context, // Usamos o context para o sf
opponent: opponent,
result: result,
myScore: myScore,
oppScore: oppScore,
date: date,
topPts: game['top_pts_name'] ?? '---',
topAst: game['top_ast_name'] ?? '---',
topRbs: game['top_rbs_name'] ?? '---',
topDef: game['top_def_name'] ?? '---',
mvp: game['mvp_name'] ?? '---',
);
}).toList(),
);
},
),
SizedBox(height: 20 * context.sf),
],
),
),
);
},
);
}
Map<String, dynamic> _calculateLeaders(List<Map<String, dynamic>> data) {
Map<String, int> ptsMap = {}; Map<String, int> astMap = {}; Map<String, int> rbsMap = {}; Map<String, String> namesMap = {};
for (var row in data) {
String pid = row['member_id'].toString();
namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido";
ptsMap[pid] = (ptsMap[pid] ?? 0) + (row['pts'] as int? ?? 0);
astMap[pid] = (astMap[pid] ?? 0) + (row['ast'] as int? ?? 0);
rbsMap[pid] = (rbsMap[pid] ?? 0) + (row['rbs'] as int? ?? 0);
}
if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0};
String getBest(Map<String, int> map) { var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key; return namesMap[bestId]!; }
int getBestVal(Map<String, int> map) => map.values.reduce((a, b) => a > b ? a : b);
return {'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap), 'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap), 'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)};
}
Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) {
return Card(
elevation: 4, margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none),
child: Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])),
child: LayoutBuilder(
builder: (context, constraints) {
final double ch = constraints.maxHeight;
final double cw = constraints.maxWidth;
return Padding(
padding: EdgeInsets.all(cw * 0.06),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis),
SizedBox(height: ch * 0.011),
SizedBox(
width: double.infinity,
child: FittedBox(
fit: BoxFit.scaleDown, alignment: Alignment.centerLeft,
child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)),
),
),
const Spacer(),
Center(child: FittedBox(fit: BoxFit.scaleDown, child: Text(statValue, style: TextStyle(fontSize: ch * 0.18, fontWeight: FontWeight.bold, color: Colors.white, height: 1.0)))),
SizedBox(height: ch * 0.015),
Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))),
const Spacer(),
Container(
width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035),
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)),
child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold)))
),
],
),
);
}
),
),
);
}
Widget _buildGameHistoryCard({
required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date,
required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp
}) {
bool isWin = result == 'V';
bool isDraw = result == 'E';
Color statusColor = isWin ? Colors.green : (isDraw ? Colors.yellow.shade700 : Colors.red);
return Container(
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))],
),
child: Column(
children: [
Padding(
padding: EdgeInsets.all(14 * context.sf),
child: Row(
children: [
Container(
width: 36 * context.sf, height: 36 * context.sf,
decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle),
child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf))),
),
SizedBox(width: 14 * context.sf),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(date, style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.w600)),
SizedBox(height: 6 * context.sf),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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 * context.sf),
child: Container(
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 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)),
),
),
Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)),
Text(title.toUpperCase(), style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white70)),
Text(playerName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)),
],
),
],
),
),
if (isHighlighted) const Icon(Icons.star, color: Colors.amber, size: 20),
],
),
const Spacer(),
Center(
child: Text(statValue, style: const TextStyle(fontSize: 38, fontWeight: FontWeight.bold, color: Colors.white)),
),
Center(
child: Text(statLabel, style: const TextStyle(fontSize: 12, color: Colors.white70)),
),
const Spacer(),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(10)),
child: const Center(child: Text('VER DETALHES', style: TextStyle(color: Colors.white, fontSize: 12))),
),
],
),
),
Divider(height: 1, color: Colors.grey.shade100, thickness: 1.5),
Container(
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(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)),
Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef)),
],
),
SizedBox(height: 8 * context.sf),
Row(
children: [
Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)),
Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs)),
],
),
SizedBox(height: 8 * context.sf),
Row(
children: [
Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)),
const Expanded(child: SizedBox()),
],
),
],
),
)
],
),
);
}
Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, String label, String value, {bool isMvp = false}) {
return Row(
children: [
Icon(icon, size: 14 * context.sf, color: color),
SizedBox(width: 4 * context.sf),
Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 11 * context.sf,
color: isMvp ? Colors.amber.shade900 : Colors.black87,
fontWeight: FontWeight.bold
),
maxLines: 1, overflow: TextOverflow.ellipsis
)
),
],
),
);
}
}

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:playmaker/controllers/login_controller.dart';
import '../widgets/login_widgets.dart';
import 'home.dart'; // <--- IMPORTANTE: Importa a tua HomeScreen
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@@ -28,40 +27,47 @@ class _LoginPageState extends State<LoginPage> {
child: ListenableBuilder(
listenable: controller,
builder: (context, child) {
return Center(
child: SingleChildScrollView(
child: Container(
width: double.infinity,
// Garante que o form não fica gigante num tablet
constraints: BoxConstraints(maxWidth: 450 * context.sf),
padding: EdgeInsets.all(32 * context.sf),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const BasketTrackHeader(),
SizedBox(height: 40 * context.sf),
LoginFormFields(controller: controller),
SizedBox(height: 24 * context.sf),
LoginButton(
controller: controller,
onLoginSuccess: () {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomeScreen()),
);
}
},
return LayoutBuilder(
builder: (context, constraints) {
final screenWidth = constraints.maxWidth;
return Center(
child: SingleChildScrollView(
child: Container(
width: screenWidth * 0.6,
constraints: const BoxConstraints(minWidth: 340),
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const BasketTrackHeader(),
const SizedBox(height: 40),
LoginFormFields(controller: controller),
const SizedBox(height: 24),
// AQUI ESTÁ A MUDANÇA PRINCIPAL
LoginButton(
controller: controller,
onLoginSuccess: () {
// Verifica se o widget ainda está no ecrã antes de navegar
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomeScreen()),
);
}
},
),
const SizedBox(height: 16),
const CreateAccountButton(),
],
),
SizedBox(height: 16 * context.sf),
const CreateAccountButton(),
],
),
),
),
),
);
},
);
},
),

View File

@@ -1,228 +0,0 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../controllers/team_controller.dart';
import '../utils/size_extension.dart'; // 👇 A MAGIA DO SF!
class StatusPage extends StatefulWidget {
const StatusPage({super.key});
@override
State<StatusPage> createState() => _StatusPageState();
}
class _StatusPageState extends State<StatusPage> {
final TeamController _teamController = TeamController();
final _supabase = Supabase.instance.client;
String? _selectedTeamId;
String _selectedTeamName = "Selecionar Equipa";
String _sortColumn = 'pts';
bool _isAscending = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: EdgeInsets.all(16.0 * context.sf),
child: InkWell(
onTap: () => _showTeamSelector(context),
child: Container(
padding: EdgeInsets.all(12 * context.sf),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15 * context.sf),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)]
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(children: [
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),
],
),
),
),
),
Expanded(
child: _selectedTeamId == null
? 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) {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: _supabase.from('games').stream(primaryKey: ['id']).eq('my_team', _selectedTeamName),
builder: (context, gamesSnapshot) {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
builder: (context, membersSnapshot) {
if (statsSnapshot.connectionState == ConnectionState.waiting || gamesSnapshot.connectionState == ConnectionState.waiting || membersSnapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator(color: Color(0xFFE74C3C)));
}
final membersData = membersSnapshot.data ?? [];
if (membersData.isEmpty) return Center(child: Text("Esta equipa não tem jogadores registados.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)));
final statsData = statsSnapshot.data ?? [];
final gamesData = gamesSnapshot.data ?? [];
final totalGamesPlayedByTeam = gamesData.where((g) => g['status'] == 'Terminado').length;
final List<Map<String, dynamic>> playerTotals = _aggregateStats(statsData, gamesData, membersData);
final teamTotals = _calculateTeamTotals(playerTotals, totalGamesPlayedByTeam);
playerTotals.sort((a, b) {
var valA = a[_sortColumn] ?? 0;
var valB = b[_sortColumn] ?? 0;
return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA);
});
return _buildStatsGrid(context, playerTotals, teamTotals);
}
);
}
);
}
),
),
],
);
}
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);
}
for (var game in games) {
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();
if (aggregated.containsKey(defName)) aggregated[defName]!['def'] += 1;
}
}
return aggregated.values.toList();
}
Map<String, dynamic> _calculateTeamTotals(List<Map<String, dynamic>> players, int teamGames) {
int tPts = 0, tAst = 0, tRbs = 0, tStl = 0, tBlk = 0, tMvp = 0, tDef = 0;
for (var p in players) {
tPts += (p['pts'] as int); tAst += (p['ast'] as int); tRbs += (p['rbs'] as int); tStl += (p['stl'] as int); tBlk += (p['blk'] as int); tMvp += (p['mvp'] as int); tDef += (p['def'] as int);
}
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) {
return Container(
color: Colors.white,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: 25 * context.sf,
headingRowColor: MaterialStateProperty.all(Colors.grey.shade100),
dataRowHeight: 60 * context.sf,
columns: [
DataColumn(label: const Text('JOGADOR')),
_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(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(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 * context.sf))),
DataCell(Center(child: Text(teamTotals['j'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)))),
_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),
]
)
],
),
),
),
);
}
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(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(BuildContext context, int value, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) {
return DataCell(Center(child: Container(
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 * 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) {
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']),
onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); },
));
},
));
}
}

View File

@@ -2,7 +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'; // 👇 IMPORTANTE: O TEU NOVO SUPERPODER
import '../widgets/team_widgets.dart';
class TeamsPage extends StatefulWidget {
const TeamsPage({super.key});
@@ -25,6 +25,7 @@ class _TeamsPageState extends State<TeamsPage> {
super.dispose();
}
// --- POPUP DE FILTROS ---
void _showFilterDialog(BuildContext context) {
showDialog(
context: context,
@@ -32,14 +33,17 @@ class _TeamsPageState extends State<TeamsPage> {
return StatefulBuilder(
builder: (context, setModalState) {
return AlertDialog(
backgroundColor: const Color(0xFF2C3E50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
backgroundColor: const Color(0xFF2C3E50), // 2. CORRIGIDO: Fundo escuro
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Filtros de pesquisa", style: TextStyle(color: Colors.white, fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
const Text(
"Filtros de pesquisa",
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
IconButton(
icon: Icon(Icons.close, color: Colors.white, size: 20 * context.sf),
icon: const Icon(Icons.close, color: Colors.white, size: 20),
onPressed: () => Navigator.pop(context),
)
],
@@ -48,10 +52,11 @@ class _TeamsPageState extends State<TeamsPage> {
mainAxisSize: MainAxisSize.min,
children: [
const Divider(color: Colors.white24),
SizedBox(height: 16 * context.sf),
const SizedBox(height: 16),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Coluna Temporada
Expanded(
child: _buildPopupColumn(
title: "TEMPORADA",
@@ -63,7 +68,8 @@ class _TeamsPageState extends State<TeamsPage> {
},
),
),
SizedBox(width: 20 * context.sf),
const SizedBox(width: 20),
// Coluna Ordenar
Expanded(
child: _buildPopupColumn(
title: "ORDENAR POR",
@@ -82,7 +88,7 @@ class _TeamsPageState extends State<TeamsPage> {
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text("CONCLUÍDO", style: TextStyle(color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold, fontSize: 14 * context.sf)),
child: const Text("CONCLUÍDO", style: TextStyle(color: Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
),
],
);
@@ -92,24 +98,29 @@ class _TeamsPageState extends State<TeamsPage> {
);
}
Widget _buildPopupColumn({required String title, required List<String> options, required String currentValue, required Function(String) onSelect}) {
Widget _buildPopupColumn({
required String title,
required List<String> options,
required String currentValue,
required Function(String) onSelect,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(color: Colors.grey, fontSize: 11 * context.sf, fontWeight: FontWeight.bold)),
SizedBox(height: 12 * context.sf),
Text(title, style: const TextStyle(color: Colors.grey, fontSize: 11, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
...options.map((opt) {
final isSelected = currentValue == opt;
return InkWell(
onTap: () => onSelect(opt),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8.0 * context.sf),
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
opt,
style: TextStyle(
// 3. CORRIGIDO: Cor do texto (Branco se não selecionado, Vermelho se selecionado)
color: isSelected ? const Color(0xFFE74C3C) : Colors.white70,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 14 * context.sf,
),
),
),
@@ -121,16 +132,15 @@ 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(
title: Text("Minhas Equipas", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)),
title: const Text("Minhas Equipas", style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: const Color(0xFFF5F7FA),
elevation: 0,
actions: [
IconButton(
icon: Icon(Icons.filter_list, color: const Color(0xFFE74C3C), size: 24 * context.sf),
icon: const Icon(Icons.filter_list, color: Color(0xFFE74C3C)),
onPressed: () => _showFilterDialog(context),
),
],
@@ -142,9 +152,8 @@ class _TeamsPageState extends State<TeamsPage> {
],
),
floatingActionButton: FloatingActionButton(
heroTag: 'add_team_btn', // 👇 A MÁGICA ESTÁ AQUI!
backgroundColor: const Color(0xFFE74C3C),
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
child: const Icon(Icons.add, color: Colors.white),
onPressed: () => _showCreateDialog(context),
),
);
@@ -152,18 +161,16 @@ class _TeamsPageState extends State<TeamsPage> {
Widget _buildSearchBar() {
return Padding(
padding: EdgeInsets.all(16.0 * context.sf),
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _searchController,
onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()),
style: TextStyle(fontSize: 16 * context.sf),
decoration: InputDecoration(
hintText: 'Pesquisar equipa...',
hintStyle: TextStyle(fontSize: 16 * context.sf),
prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * context.sf),
prefixIcon: const Icon(Icons.search, color: Color(0xFFE74C3C)),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * context.sf), borderSide: BorderSide.none),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none),
),
),
);
@@ -173,30 +180,56 @@ class _TeamsPageState extends State<TeamsPage> {
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 * context.sf)));
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text("Nenhuma equipa encontrada."));
}
var data = List<Map<String, dynamic>>.from(snapshot.data!);
if (_selectedSeason != 'Todas') data = data.where((t) => t['season'] == _selectedSeason).toList();
if (_searchQuery.isNotEmpty) data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList();
// --- 1. FILTROS ---
if (_selectedSeason != 'Todas') {
data = data.where((t) => t['season'] == _selectedSeason).toList();
}
if (_searchQuery.isNotEmpty) {
data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList();
}
// --- 2. ORDENAÇÃO (FAVORITOS PRIMEIRO) ---
data.sort((a, b) {
// Apanhar o estado de favorito (tratando null como false)
bool favA = a['is_favorite'] ?? false;
bool favB = b['is_favorite'] ?? false;
if (favA && !favB) return -1;
if (!favA && favB) return 1;
if (_currentSort == 'Nome') return a['name'].toString().compareTo(b['name'].toString());
else return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString());
// REGRA 1: Favoritos aparecem sempre primeiro
if (favA && !favB) return -1; // A sobe
if (!favA && favB) return 1; // B sobe
// REGRA 2: Se o estado de favorito for igual, aplica o filtro do utilizador
if (_currentSort == 'Nome') {
return a['name'].toString().compareTo(b['name'].toString());
} else { // Recentes
return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString());
}
});
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf),
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: data.length,
itemBuilder: (context, index) {
final team = Team.fromMap(data[index]);
// Navegação para estatísticas
return GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => TeamStatsPage(team: team)),
);
},
child: TeamCard(
team: team,
controller: controller,
@@ -210,132 +243,13 @@ class _TeamsPageState extends State<TeamsPage> {
}
void _showCreateDialog(BuildContext context) {
showDialog(context: context, builder: (context) => CreateTeamDialog(onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl)));
}
}
// --- TEAM CARD ---
class TeamCard extends StatelessWidget {
final Team team;
final TeamController controller;
final VoidCallback onFavoriteTap;
const TeamCard({super.key, required this.team, required this.controller, required this.onFavoriteTap});
@override
Widget build(BuildContext context) {
return Card(
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 * context.sf, vertical: 8 * context.sf),
leading: Stack(
clipBehavior: Clip.none,
children: [
CircleAvatar(
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 * context.sf)) : null,
),
Positioned(
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 * 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 * context.sf), overflow: TextOverflow.ellipsis),
subtitle: Padding(
padding: EdgeInsets.only(top: 6.0 * context.sf),
child: Row(
children: [
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 * context.sf));
},
),
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 * 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) {
showDialog(
context: context,
builder: (context) => AlertDialog(
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 * context.sf))),
TextButton(onPressed: () { controller.deleteTeam(team.id); Navigator.pop(context); }, child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * context.sf))),
],
builder: (context) => CreateTeamDialog(
onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl),
),
);
}
}
// --- DIALOG DE CRIAÇÃO ---
class CreateTeamDialog extends StatefulWidget {
final Function(String name, String season, String imageUrl) onConfirm;
const CreateTeamDialog({super.key, required this.onConfirm});
@override
State<CreateTeamDialog> createState() => _CreateTeamDialogState();
}
class _CreateTeamDialogState extends State<CreateTeamDialog> {
final TextEditingController _nameController = TextEditingController();
final TextEditingController _imageController = TextEditingController();
String _selectedSeason = '2024/25';
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
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 * 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 * 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 * context.sf))),
ElevatedButton(
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 * context.sf)),
),
],
);
}
}

View File

@@ -0,0 +1 @@
import 'package:flutter/material.dart';

View File

@@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
// Esta extensão adiciona o superpoder "sf" ao BuildContext
extension SizeExtension on BuildContext {
double get sf {
final double wScreen = MediaQuery.of(this).size.width;
final double hScreen = MediaQuery.of(this).size.height;
// Calcula e devolve a escala na hora!
return math.min(wScreen, hScreen) / 400;
}
}

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:playmaker/pages/PlacarPage.dart';
import 'package:playmaker/pages/PlacarPage.dart'; // Garante que o import está correto
import '../controllers/team_controller.dart';
import '../controllers/game_controller.dart';
@@ -7,9 +7,8 @@ import '../controllers/game_controller.dart';
class GameResultCard extends StatelessWidget {
final String gameId;
final String myTeam, opponentTeam, myScore, opponentScore, status, season;
final String? myTeamLogo;
final String? opponentTeamLogo;
final double sf; // NOVA VARIÁVEL DE ESCALA
final String? myTeamLogo; // NOVA VARIÁVEL
final String? opponentTeamLogo; // NOVA VARIÁVEL
const GameResultCard({
super.key,
@@ -20,70 +19,71 @@ class GameResultCard extends StatelessWidget {
required this.opponentScore,
required this.status,
required this.season,
this.myTeamLogo,
this.opponentTeamLogo,
required this.sf, // OBRIGATÓRIO RECEBER A ESCALA
this.myTeamLogo, // ADICIONADO AO CONSTRUTOR
this.opponentTeamLogo, // ADICIONADO AO CONSTRUTOR
});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 16 * sf),
padding: EdgeInsets.all(16 * sf),
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20 * sf),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)],
borderRadius: BorderRadius.circular(20),
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 10)],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, sf)),
_buildScoreCenter(context, gameId, sf),
Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, sf)),
// Passamos a imagem para a função
Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo)),
_buildScoreCenter(context, gameId),
// Passamos a imagem para a função
Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo)),
],
),
);
}
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf) {
// ATUALIZADO para desenhar a imagem
Widget _buildTeamInfo(String name, Color color, String? logoUrl) {
return Column(
children: [
CircleAvatar(
radius: 24 * sf, // Ajuste do tamanho do logo
backgroundColor: color,
backgroundImage: (logoUrl != null && logoUrl.isNotEmpty)
? NetworkImage(logoUrl)
: null,
child: (logoUrl == null || logoUrl.isEmpty)
? Icon(Icons.shield, color: Colors.white, size: 24 * sf)
? const Icon(Icons.shield, color: Colors.white)
: null,
),
SizedBox(height: 6 * sf),
const SizedBox(height: 4),
Text(name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2, // Permite 2 linhas para nomes compridos não cortarem
),
],
);
}
Widget _buildScoreCenter(BuildContext context, String id, double sf) {
Widget _buildScoreCenter(BuildContext context, String id) {
return Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
_scoreBox(myScore, Colors.green, sf),
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf)),
_scoreBox(opponentScore, Colors.grey, sf),
_scoreBox(myScore, Colors.green),
const Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
_scoreBox(opponentScore, Colors.grey),
],
),
SizedBox(height: 10 * sf),
const SizedBox(height: 8),
TextButton.icon(
onPressed: () {
// NAVEGAÇÃO PARA O PLACAR (Usando o ID real)
Navigator.push(
context,
MaterialPageRoute(
@@ -95,25 +95,25 @@ class GameResultCard extends StatelessWidget {
),
);
},
icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: const Color(0xFFE74C3C)),
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
icon: const Icon(Icons.play_circle_fill, size: 16, color: Color(0xFFE74C3C)),
label: const Text("RETORNAR", style: TextStyle(fontSize: 10, color: Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
style: TextButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1),
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)),
padding: const EdgeInsets.symmetric(horizontal: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
visualDensity: VisualDensity.compact,
),
),
SizedBox(height: 6 * sf),
Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(status, style: const TextStyle(fontSize: 10, color: Colors.blue, fontWeight: FontWeight.bold)),
],
);
}
Widget _scoreBox(String pts, Color c, double sf) => Container(
padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)),
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
Widget _scoreBox(String pts, Color c) => Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8)),
child: Text(pts, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
);
}
@@ -121,13 +121,11 @@ class GameResultCard extends StatelessWidget {
class CreateGameDialogManual extends StatefulWidget {
final TeamController teamController;
final GameController gameController;
final double sf; // NOVA VARIÁVEL DE ESCALA
const CreateGameDialogManual({
super.key,
required this.teamController,
required this.gameController,
required this.sf,
required this.gameController
});
@override
@@ -155,46 +153,33 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * widget.sf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * widget.sf)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: const Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _seasonController,
style: TextStyle(fontSize: 14 * widget.sf),
decoration: InputDecoration(
labelText: 'Temporada',
labelStyle: TextStyle(fontSize: 14 * widget.sf),
border: const OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today, size: 20 * widget.sf)
),
decoration: const InputDecoration(labelText: 'Temporada', border: OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today)),
),
SizedBox(height: 15 * widget.sf),
const SizedBox(height: 15),
_buildSearch(label: "Minha Equipa", controller: _myTeamController, sf: widget.sf),
_buildSearch(label: "Minha Equipa", controller: _myTeamController),
Padding(
padding: EdgeInsets.symmetric(vertical: 10 * widget.sf),
child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * widget.sf))
),
const Padding(padding: EdgeInsets.symmetric(vertical: 8), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey))),
_buildSearch(label: "Adversário", controller: _opponentController, sf: widget.sf),
_buildSearch(label: "Adversário", controller: _opponentController),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf))
),
TextButton(onPressed: () => Navigator.pop(context), child: const Text('CANCELAR')),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * widget.sf)),
padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: _isLoading ? null : () async {
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
@@ -225,14 +210,15 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
}
},
child: _isLoading
? SizedBox(width: 20 * widget.sf, height: 20 * widget.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * widget.sf)),
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: const Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
],
);
}
Widget _buildSearch({required String label, required TextEditingController controller, required double sf}) {
// ATUALIZADO para usar Map e mostrar a imagem na lista de pesquisa
Widget _buildSearch({required String label, required TextEditingController controller}) {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: widget.teamController.teamsStream,
builder: (context, snapshot) {
@@ -256,9 +242,10 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0,
borderRadius: BorderRadius.circular(8 * sf),
borderRadius: BorderRadius.circular(8),
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 250 * sf, maxWidth: MediaQuery.of(context).size.width * 0.7),
// Ajuste do tamanho máximo do pop-up de sugestões
constraints: BoxConstraints(maxHeight: 250, maxWidth: MediaQuery.of(context).size.width * 0.7),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
@@ -270,16 +257,15 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
return ListTile(
leading: CircleAvatar(
radius: 20 * 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 * sf)
? const Icon(Icons.shield, color: Colors.grey)
: null,
),
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * sf)),
title: Text(name, style: const TextStyle(fontWeight: FontWeight.bold)),
onTap: () {
onSelected(option);
},
@@ -302,11 +288,9 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
return TextField(
controller: txtCtrl,
focusNode: node,
style: TextStyle(fontSize: 14 * sf),
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(fontSize: 14 * sf),
prefixIcon: Icon(Icons.search, size: 20 * sf),
prefixIcon: const Icon(Icons.search),
border: const OutlineInputBorder()
),
);

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:playmaker/classe/home.config.dart';
class StatCard extends StatelessWidget {
final String title;
@@ -9,11 +10,6 @@ class StatCard extends StatelessWidget {
final IconData icon;
final bool isHighlighted;
final VoidCallback? onTap;
// Variáveis novas para que o tamanho não fique preso à HomeConfig
final double sf;
final double cardWidth;
final double cardHeight;
const StatCard({
super.key,
@@ -25,30 +21,27 @@ class StatCard extends StatelessWidget {
required this.icon,
this.isHighlighted = false,
this.onTap,
this.sf = 1.0, // Default 1.0 para não dar erro se não passares o valor
required this.cardWidth,
required this.cardHeight,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: cardWidth,
height: cardHeight,
return Container(
width: HomeConfig.cardwidthPadding,
height: HomeConfig.cardheightPadding,
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20 * sf),
borderRadius: BorderRadius.circular(20),
side: isHighlighted
? BorderSide(color: Colors.amber, width: 2 * sf)
? BorderSide(color: Colors.amber, width: 2)
: BorderSide.none,
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20 * sf),
borderRadius: BorderRadius.circular(20),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20 * sf),
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
@@ -59,14 +52,13 @@ class StatCard extends StatelessWidget {
),
),
child: Padding(
padding: EdgeInsets.all(16.0 * sf),
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Cabeçalho
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
@@ -74,12 +66,12 @@ class StatCard extends StatelessWidget {
children: [
Text(
title.toUpperCase(),
style: TextStyle(fontSize: 11 * sf, fontWeight: FontWeight.bold, color: Colors.white70),
style: HomeConfig.titleStyle,
),
SizedBox(height: 2 * sf),
SizedBox(height: 5),
Text(
playerName,
style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold, color: Colors.white),
style: HomeConfig.playerNameStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -88,38 +80,38 @@ class StatCard extends StatelessWidget {
),
if (isHighlighted)
Container(
padding: EdgeInsets.all(6 * sf),
decoration: const BoxDecoration(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.amber,
shape: BoxShape.circle,
),
child: Icon(
Icons.star,
size: 16 * sf,
size: 20,
color: Colors.white,
),
),
],
),
SizedBox(height: 8 * sf),
SizedBox(height: 10),
// Ícone
Container(
width: 45 * sf,
height: 45 * sf,
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 24 * sf,
size: 30,
color: Colors.white,
),
),
const Spacer(),
Spacer(),
// Estatística
Center(
@@ -127,26 +119,26 @@ class StatCard extends StatelessWidget {
children: [
Text(
statValue,
style: TextStyle(fontSize: 34 * sf, fontWeight: FontWeight.bold, color: Colors.white),
style: HomeConfig.statValueStyle,
),
SizedBox(height: 2 * sf),
SizedBox(height: 5),
Text(
statLabel.toUpperCase(),
style: TextStyle(fontSize: 12 * sf, color: Colors.white70),
style: HomeConfig.statLabelStyle,
),
],
),
),
const Spacer(),
Spacer(),
// Botão
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 8 * sf),
padding: EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(10 * sf),
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: Text(
@@ -154,7 +146,7 @@ class StatCard extends StatelessWidget {
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 11 * sf,
fontSize: 14,
letterSpacing: 1,
),
),
@@ -177,12 +169,12 @@ class SportGrid extends StatelessWidget {
const SportGrid({
super.key,
required this.children,
this.spacing = 20.0, // Valor padrão se não for passado nada
this.spacing = HomeConfig.cardSpacing,
});
@override
Widget build(BuildContext context) {
if (children.isEmpty) return const SizedBox();
if (children.isEmpty) return SizedBox();
return Column(
children: [

View File

@@ -1,146 +1,194 @@
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
import 'package:playmaker/controllers/login_controller.dart';
import 'package:playmaker/pages/RegisterPage.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
class BasketTrackHeader extends StatelessWidget {
const BasketTrackHeader({super.key});
class BasketTrackHeader extends StatelessWidget {
const BasketTrackHeader({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(
width: 200 * context.sf, // Ajusta o tamanho da imagem suavemente
height: 200 * context.sf,
child: Image.asset(
'assets/playmaker-logos.png',
fit: BoxFit.contain,
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
// TAMANHOS AUMENTADOS para tablets
final logoSize = screenWidth > 600 ? 400.0 : 300.0; // ↑ Aumentado
final titleFontSize = screenWidth > 600 ? 48.0 : 36.0; // ↑ Aumentado
final subtitleFontSize = screenWidth > 600 ? 22.0 : 18.0; // ↑ Aumentado
return Column(
children: [
Container(
width: logoSize,
height: logoSize,
child: Image.asset(
'assets/playmaker-logo.png',
fit: BoxFit.contain,
),
),
),
Text(
'BasketTrack',
style: TextStyle(
fontSize: 36 * context.sf,
fontWeight: FontWeight.bold,
color: Colors.grey[900],
SizedBox(height: screenWidth > 600 ? 1.0 : 1.0),
Text(
'BasketTrack',
style: TextStyle(
fontSize: titleFontSize,
fontWeight: FontWeight.bold,
color: Colors.grey[900],
),
),
),
SizedBox(height: 6 * context.sf),
Text(
'Gere as tuas equipas e estatísticas',
style: TextStyle(
fontSize: 16 * context.sf,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
SizedBox(height: screenWidth > 600 ? 1.0 : 1.0),
Text(
'Gere as tuas equipas e estatísticas',
style: TextStyle(
fontSize: subtitleFontSize,
color: Colors.grey[600],
fontWeight: FontWeight.w500, // ↑ Adicionado peso da fonte
),
textAlign: TextAlign.center,
),
textAlign: TextAlign.center,
),
],
);
],
);
}
}
}
class LoginFormFields extends StatelessWidget {
final LoginController controller;
class LoginFormFields extends StatelessWidget {
final LoginController controller;
const LoginFormFields({super.key, required this.controller});
const LoginFormFields({super.key, required this.controller});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final verticalPadding = screenWidth > 600 ? 22.0 : 16.0;
return Column(
children: [
TextField(
controller: controller.emailController,
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'E-mail',
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf),
prefixIcon: const Icon(Icons.email_outlined),
// O erro agora vem diretamente do controller
errorText: controller.emailError,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16),
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 20 * context.sf),
const SizedBox(height: 20),
TextField(
controller: controller.passwordController,
obscureText: controller.obscurePassword,
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf),
prefixIcon: const Icon(Icons.lock_outlined),
errorText: controller.passwordError,
suffixIcon: IconButton(
icon: Icon(
controller.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
size: 22 * context.sf
),
icon: Icon(controller.obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined),
onPressed: controller.togglePasswordVisibility,
),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16),
),
),
],
);
}
}
class LoginButton extends StatelessWidget {
final LoginController controller;
final VoidCallback onLoginSuccess;
class LoginButton extends StatelessWidget {
final LoginController controller;
final VoidCallback onLoginSuccess;
const LoginButton({
super.key,
required this.controller,
required this.onLoginSuccess,
});
const LoginButton({super.key, required this.controller, required this.onLoginSuccess});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
// BOTÕES MAIORES
final buttonHeight = screenWidth > 600 ? 70.0 : 58.0; // ↑ Aumentado
final fontSize = screenWidth > 600 ? 22.0 : 18.0; // ↑ Aumentado
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 58 * context.sf,
child: ElevatedButton(
onPressed: controller.isLoading ? null : () async {
final success = await controller.login();
if (success) onLoginSuccess();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
elevation: 3,
return SizedBox(
width: double.infinity,
height: buttonHeight,
child: ElevatedButton(
onPressed: controller.isLoading ? null : () async {
final success = await controller.login();
if (success) {
onLoginSuccess();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14), // ↑ Bordas mais arredondadas
),
elevation: 3, // ↑ Sombra mais pronunciada
),
child: controller.isLoading
? SizedBox(
width: 28, // ↑ Aumentado
height: 28, // ↑ Aumentado
child: CircularProgressIndicator(
strokeWidth: 3, // ↑ Aumentado
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'Entrar',
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w700, //
),
),
),
child: controller.isLoading
? SizedBox(
width: 28 * context.sf, height: 28 * context.sf,
child: const CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(Colors.white)),
)
: Text('Entrar', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
),
);
);
}
}
}
class CreateAccountButton extends StatelessWidget {
const CreateAccountButton({super.key});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final buttonHeight = screenWidth > 600 ? 70.0 : 58.0;
final fontSize = screenWidth > 600 ? 22.0 : 18.0;
return SizedBox(
width: double.infinity,
height: 58 * context.sf,
height: buttonHeight,
child: OutlinedButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterPage()));
// Navega para a página de registo que criaste
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const RegisterPage()),
);
},
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFE74C3C),
side: BorderSide(color: const Color(0xFFE74C3C), width: 2 * context.sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
side: const BorderSide(color: Color(0xFFE74C3C), width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: Text(
'Criar Conta',
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w700,
),
),
child: Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
),
);
}
}
}

View File

@@ -4,104 +4,70 @@ import 'package:playmaker/controllers/placar_controller.dart';
// --- PLACAR SUPERIOR ---
class TopScoreboard extends StatelessWidget {
final PlacarController controller;
final double sf;
const TopScoreboard({super.key, required this.controller, required this.sf});
const TopScoreboard({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 10 * sf, horizontal: 35 * sf),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
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),
color: const Color(0xFF16202C),
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(15), bottomRight: Radius.circular(15)),
border: Border.all(color: Colors.white, width: 2),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false, sf),
SizedBox(width: 30 * sf),
_buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false),
const SizedBox(width: 25),
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)
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
decoration: BoxDecoration(color: const Color(0xFF2C3E50), borderRadius: BorderRadius.circular(6)),
child: Text(controller.formatTime(), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, fontFamily: 'monospace')),
),
const SizedBox(height: 5),
Text("PERÍODO ${controller.currentQuarter}", style: const TextStyle(color: Colors.orangeAccent, fontSize: 14, fontWeight: FontWeight.bold)),
],
),
SizedBox(width: 30 * sf),
_buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true, sf),
const SizedBox(width: 25),
_buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true),
],
),
);
}
Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp, double sf) {
int displayFouls = fouls > 5 ? 5 : fouls;
Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp) {
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)
),
margin: const EdgeInsets.symmetric(horizontal: 3),
width: 12, height: 12,
decoration: BoxDecoration(shape: BoxShape.circle, color: index < timeouts ? Colors.yellow : Colors.grey.shade600, border: Border.all(color: Colors.black26)),
)),
);
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());
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: isOpp
? [
Column(children: [_scoreBox(score, color), const SizedBox(height: 4), Text("FALTAS: $fouls", style: TextStyle(color: fouls >= 5 ? Colors.red : Colors.yellowAccent, fontSize: 12, fontWeight: FontWeight.bold)), timeoutIndicators]),
const SizedBox(width: 15),
Text(name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold))
]
: [
Text(name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(width: 15),
Column(children: [_scoreBox(score, color), const SizedBox(height: 4), Text("FALTAS: $fouls", style: TextStyle(color: fouls >= 5 ? Colors.red : Colors.yellowAccent, fontSize: 12, fontWeight: FontWeight.bold)), timeoutIndicators])
]
);
}
Widget _scoreBox(int score, Color color, double sf) => Container(
width: 58 * sf, height: 45 * sf,
Widget _scoreBox(int score, Color color) => Container(
width: 50, height: 40,
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)),
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(6)),
child: Text(score.toString(), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)),
);
}
@@ -109,9 +75,7 @@ class TopScoreboard extends StatelessWidget {
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});
const BenchPlayersList({super.key, required this.controller, required this.isOpponent});
@override
Widget build(BuildContext context) {
@@ -127,45 +91,24 @@ class BenchPlayersList extends StatelessWidget {
final bool isFouledOut = fouls >= 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))]
),
margin: const EdgeInsets.only(bottom: 5),
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
)
),
backgroundColor: isFouledOut ? Colors.grey.shade700 : teamColor,
child: Text(num, style: TextStyle(color: isFouledOut ? Colors.red.shade300 : Colors.white, fontSize: 14, 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
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)),
feedback: Material(color: Colors.transparent, child: CircleAvatar(backgroundColor: teamColor, child: Text(num, style: const TextStyle(color: Colors.white)))),
childWhenDragging: const Opacity(opacity: 0.5, child: SizedBox(width: 40, height: 40)),
child: avatarUI,
);
}).toList(),
@@ -178,9 +121,8 @@ 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});
const PlayerCourtCard({super.key, required this.controller, required this.name, required this.isOpponent});
@override
Widget build(BuildContext context) {
@@ -194,40 +136,38 @@ class PlayerCourtCard extends StatelessWidget {
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)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(8)),
child: Text(name, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
),
),
childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, name, stats, teamColor, false, false, sf)),
childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, name, stats, teamColor, false, false)),
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_")) {
}
else if (action.startsWith("bench_")) {
controller.handleSubbing(context, action, name, isOpponent);
}
},
builder: (context, candidateData, rejectedData) {
bool isSubbing = candidateData.any((data) => data != null && (data.startsWith("bench_my_") || data.startsWith("bench_opp_")));
bool isActionHover = candidateData.any((data) => data != null && (data.startsWith("add_") || data.startsWith("sub_") || data.startsWith("miss_")));
return _playerCardUI(number, name, stats, teamColor, isSubbing, isActionHover, sf);
return _playerCardUI(number, name, stats, teamColor, isSubbing, isActionHover);
},
),
);
}
Widget _playerCardUI(String number, String name, Map<String, int> stats, Color teamColor, bool isSubbing, bool isActionHover, double sf) {
Widget _playerCardUI(String number, String name, Map<String, int> stats, Color teamColor, bool isSubbing, bool isActionHover) {
bool isFouledOut = stats["fls"]! >= 5;
Color bgColor = isFouledOut ? Colors.red.shade50 : Colors.white;
Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white;
Color borderColor = isFouledOut ? Colors.redAccent : Colors.transparent;
if (isSubbing) {
bgColor = Colors.blue.shade50; borderColor = Colors.blue;
} else if (isActionHover && !isFouledOut) {
bgColor = Colors.orange.shade50; borderColor = Colors.orange;
}
if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = Colors.blue; }
else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = Colors.orange; }
int fgm = stats["fgm"]!;
int fga = stats["fga"]!;
@@ -235,50 +175,31 @@ class PlayerCourtCard extends StatelessWidget {
String displayName = name.length > 12 ? "${name.substring(0, 10)}..." : name;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(11 * sf),
border: Border.all(color: borderColor, width: 1.8 * sf),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5 * sf, offset: Offset(2 * sf, 3.5 * sf))],
color: bgColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: borderColor, width: 2),
boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 3))],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(9 * sf),
child: IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40, height: 40,
decoration: BoxDecoration(color: isFouledOut ? Colors.grey : teamColor, borderRadius: BorderRadius.circular(8)),
alignment: Alignment.center,
child: Text(number, style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold)),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min,
children: [
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: 22 * sf, fontWeight: FontWeight.bold)),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 7 * 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.5 * sf),
Text(
"${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)",
style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[700], fontWeight: FontWeight.w600)
),
Text(
"${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls",
style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[500], fontWeight: FontWeight.w600)
),
],
),
),
Text(displayName, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: isFouledOut ? Colors.red : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)),
const SizedBox(height: 1),
Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 11, color: isFouledOut ? Colors.red : Colors.grey[700], fontWeight: FontWeight.w600)),
Text("${stats["ast"]} Ast | ${stats["rbs"]} Rbs | ${stats["fls"]} Fls", style: TextStyle(fontSize: 11, color: isFouledOut ? Colors.red : Colors.grey, fontWeight: FontWeight.w500)),
],
),
),
],
),
);
}
@@ -287,133 +208,130 @@ class PlayerCourtCard extends StatelessWidget {
// --- PAINEL DE BOTÕES DE AÇÃO ---
class ActionButtonsPanel extends StatelessWidget {
final PlacarController controller;
final double sf;
const ActionButtonsPanel({super.key, required this.controller, required this.sf});
const ActionButtonsPanel({super.key, required this.controller});
@override
Widget build(BuildContext context) {
final double baseSize = 65 * sf; // Reduzido (Antes era 75)
final double feedSize = 82 * sf; // Reduzido (Antes era 95)
final double gap = 7 * sf;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_columnBtn([
_dragAndTargetBtn("M1", Colors.redAccent, "miss_1", baseSize, feedSize, sf),
_dragAndTargetBtn("1", Colors.orange, "add_pts_1", baseSize, feedSize, sf),
_dragAndTargetBtn("1", Colors.orange, "sub_pts_1", baseSize, feedSize, sf, isX: true),
_dragAndTargetBtn("STL", Colors.green, "add_stl", baseSize, feedSize, sf),
], gap),
SizedBox(width: gap * 1),
_actionBtn("T.O", const Color(0xFF1E5BB2), () => controller.useTimeout(false), labelSize: 20),
_dragAndTargetBtn("1", Colors.orange, "add_pts_1"),
_dragAndTargetBtn("1", Colors.orange, "sub_pts_1", isX: true),
_dragAndTargetBtn("STL", Colors.green, "add_stl"),
]),
const SizedBox(width: 15),
_columnBtn([
_dragAndTargetBtn("M2", Colors.redAccent, "miss_2", baseSize, feedSize, sf),
_dragAndTargetBtn("2", Colors.orange, "add_pts_2", baseSize, feedSize, sf),
_dragAndTargetBtn("2", Colors.orange, "sub_pts_2", baseSize, feedSize, sf, isX: true),
_dragAndTargetBtn("AST", Colors.blueGrey, "add_ast", baseSize, feedSize, sf),
], gap),
SizedBox(width: gap * 1),
_dragAndTargetBtn("M2", Colors.redAccent, "miss_2"),
_dragAndTargetBtn("2", Colors.orange, "add_pts_2"),
_dragAndTargetBtn("2", Colors.orange, "sub_pts_2", isX: true),
_dragAndTargetBtn("AST", Colors.blueGrey, "add_ast"),
]),
const SizedBox(width: 15),
_columnBtn([
_dragAndTargetBtn("M3", Colors.redAccent, "miss_3", baseSize, feedSize, sf),
_dragAndTargetBtn("3", Colors.orange, "add_pts_3", baseSize, feedSize, sf),
_dragAndTargetBtn("3", Colors.orange, "sub_pts_3", baseSize, feedSize, sf, isX: true),
_dragAndTargetBtn("TOV", Colors.redAccent, "add_tov", baseSize, feedSize, sf),
], gap),
SizedBox(width: gap * 1),
_dragAndTargetBtn("M3", Colors.redAccent, "miss_3"),
_dragAndTargetBtn("3", Colors.orange, "add_pts_3"),
_dragAndTargetBtn("3", Colors.orange, "sub_pts_3", isX: true),
_dragAndTargetBtn("TOV", Colors.redAccent, "add_tov"),
]),
const SizedBox(width: 15),
_columnBtn([
_dragAndTargetBtn("ORB", const Color(0xFF1E2A38), "add_orb", baseSize, feedSize, sf, icon: Icons.sports_basketball),
_dragAndTargetBtn("DRB", const Color(0xFF1E2A38), "add_drb", baseSize, feedSize, sf, icon: Icons.sports_basketball),
_dragAndTargetBtn("BLK", Colors.deepPurple, "add_blk", baseSize, feedSize, sf, icon: Icons.front_hand),
], gap),
_actionBtn("T.O", const Color(0xFFD92C2C), () => controller.useTimeout(true), labelSize: 20),
_dragAndTargetBtn("ORB", const Color(0xFF1E2A38), "add_rbs", icon: Icons.sports_basketball),
_dragAndTargetBtn("DRB", const Color(0xFF1E2A38), "add_rbs", icon: Icons.sports_basketball),
_dragAndTargetBtn("BLK", Colors.deepPurple, "add_blk", icon: Icons.front_hand),
]),
const SizedBox(width: 15),
_columnBtn([
])
],
);
}
Widget _columnBtn(List<Widget> children, double gap) {
return Column(
mainAxisSize: MainAxisSize.min,
children: children.map((c) => Padding(padding: EdgeInsets.only(bottom: gap), child: c)).toList()
);
}
// Mantenha os métodos _columnBtn, _dragAndTargetBtn, _actionBtn e _circle exatamente como estão
Widget _columnBtn(List<Widget> children) => Column(mainAxisSize: MainAxisSize.min, children: children.map((c) => Padding(padding: const EdgeInsets.only(bottom: 8), child: c)).toList());
Widget _dragAndTargetBtn(String label, Color color, String actionData, double baseSize, double feedSize, double sf, {IconData? icon, bool isX = false}) {
Widget _dragAndTargetBtn(String label, Color color, String actionData, {IconData? icon, bool isX = false}) {
return Draggable<String>(
data: actionData,
feedback: _circle(label, color, icon, true, baseSize, feedSize, sf, isX: isX),
childWhenDragging: Opacity(
opacity: 0.5,
child: _circle(label, color, icon, false, baseSize, feedSize, sf, isX: isX)
),
feedback: _circle(label, color, icon, true, isX: isX),
childWhenDragging: Opacity(opacity: 0.5, child: _circle(label, color, icon, false, isX: isX)),
child: DragTarget<String>(
onAcceptWithDetails: (details) {},
onAcceptWithDetails: (details) {
final playerData = details.data;
// Requer um BuildContext, não acessível diretamente no Stateless, então não fazemos nada aqui.
// O target real está no PlayerCourtCard!
},
builder: (context, candidateData, rejectedData) {
bool isHovered = candidateData.any((data) => data != null && data.startsWith("player_"));
return Transform.scale(
scale: isHovered ? 1.15 : 1.0,
child: Container(
decoration: isHovered ? BoxDecoration(shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.white, blurRadius: 10 * sf, spreadRadius: 3 * sf)]) : null,
child: _circle(label, color, icon, false, baseSize, feedSize, sf, isX: isX)
),
child: Container(decoration: isHovered ? BoxDecoration(shape: BoxShape.circle, boxShadow: const [BoxShadow(color: Colors.white, blurRadius: 10, spreadRadius: 3)]) : null, child: _circle(label, color, icon, false, isX: isX)),
);
}
),
);
}
Widget _circle(String label, Color color, IconData? icon, bool isFeed, double baseSize, double feedSize, double sf, {bool isX = false}) {
double size = isFeed ? feedSize : baseSize;
Widget _actionBtn(String label, Color color, VoidCallback onTap, {IconData? icon, bool isX = false, double labelSize = 24}) {
return GestureDetector(onTap: onTap, child: _circle(label, color, icon, false, fontSize: labelSize, isX: isX));
}
Widget _circle(String label, Color color, IconData? icon, bool isFeed, {double fontSize = 20, bool isX = false}) {
Widget content;
bool isPointBtn = label == "1" || label == "2" || label == "3" || label == "M1" || label == "M2" || label == "M3";
bool isPointBtn = label == "1" || label == "2" || label == "3" || label == "M2" || label == "M3";
bool isBlkBtn = label == "BLK";
if (isPointBtn) {
content = Stack(
alignment: Alignment.center,
children: [
Container(width: size * 0.75, height: size * 0.75, decoration: const BoxDecoration(color: Colors.black, shape: BoxShape.circle)),
Icon(Icons.sports_basketball, color: color, size: size * 0.9),
Container(width: isFeed ? 55 : 45, height: isFeed ? 55 : 45, decoration: const BoxDecoration(color: Colors.black, shape: BoxShape.circle)),
Icon(Icons.sports_basketball, color: color, size: isFeed ? 65 : 55),
Stack(
children: [
Text(label, style: TextStyle(fontSize: size * 0.38, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = size * 0.05..color = Colors.white, decoration: TextDecoration.none)),
Text(label, style: TextStyle(fontSize: size * 0.38, fontWeight: FontWeight.w900, color: Colors.black, decoration: TextDecoration.none)),
Text(label, style: TextStyle(fontSize: isFeed ? 26 : 22, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = 3..color = Colors.white, decoration: TextDecoration.none)),
Text(label, style: TextStyle(fontSize: isFeed ? 26 : 22, fontWeight: FontWeight.w900, color: Colors.black, decoration: TextDecoration.none)),
],
),
],
);
} else if (isBlkBtn) {
}
else if (isBlkBtn) {
content = Stack(
alignment: Alignment.center,
children: [
Icon(Icons.front_hand, color: const Color.fromARGB(207, 56, 52, 52), size: size * 0.75),
Icon(Icons.front_hand, color: const Color.fromARGB(207, 56, 52, 52), size: isFeed ? 55 : 45),
Stack(
alignment: Alignment.center,
children: [
Text(label, style: TextStyle(fontSize: size * 0.28, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = size * 0.05..color = Colors.black, decoration: TextDecoration.none)),
Text(label, style: TextStyle(fontSize: size * 0.28, fontWeight: FontWeight.w900, color: Colors.white, decoration: TextDecoration.none)),
Text(label, style: TextStyle(fontSize: isFeed ? 18 : 16, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = 3..color = Colors.black, decoration: TextDecoration.none)),
Text(label, style: TextStyle(fontSize: isFeed ? 18 : 16, fontWeight: FontWeight.w900, color: Colors.white, decoration: TextDecoration.none)),
],
),
],
);
} else if (icon != null) {
content = Icon(icon, color: Colors.white, size: size * 0.5);
content = Icon(icon, color: Colors.white, size: 30);
} else {
content = Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: size * 0.35, decoration: TextDecoration.none));
content = Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: fontSize, decoration: TextDecoration.none));
}
return Stack(
clipBehavior: Clip.none,
alignment: Alignment.bottomRight,
clipBehavior: Clip.none, alignment: Alignment.bottomRight,
children: [
Container(
width: size, height: size,
decoration: (isPointBtn || isBlkBtn) ? const BoxDecoration(color: Colors.transparent) : BoxDecoration(gradient: RadialGradient(colors: [color.withOpacity(0.7), color], radius: 0.8), shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black38, blurRadius: 6 * sf, offset: Offset(0, 3 * sf))]),
alignment: Alignment.center,
child: content,
width: isFeed ? 70 : 60, height: isFeed ? 70 : 60,
decoration: (isPointBtn || isBlkBtn) ? const BoxDecoration(color: Colors.transparent) : BoxDecoration(gradient: RadialGradient(colors: [color.withOpacity(0.7), color], radius: 0.8), shape: BoxShape.circle, boxShadow: const [BoxShadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 3))]),
alignment: Alignment.center, child: content,
),
if (isX) Positioned(top: 0, right: 0, child: Container(decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), child: Icon(Icons.cancel, color: Colors.red, size: size * 0.4))),
if (isX) Positioned(top: 0, right: 0, child: Container(decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), child: Icon(Icons.cancel, color: Colors.red, size: isFeed ? 28 : 24))),
],
);
}
}

View File

@@ -1,24 +1,41 @@
import 'package:flutter/material.dart';
import '../controllers/register_controller.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
import '../controllers/register_controller.dart'; // Garante que o caminho está certo
class RegisterHeader extends StatelessWidget {
const RegisterHeader({super.key});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final logoSize = screenWidth > 600 ? 150.0 : 100.0;
final titleFontSize = screenWidth > 600 ? 48.0 : 36.0;
final subtitleFontSize = screenWidth > 600 ? 22.0 : 18.0;
return Column(
children: [
Icon(Icons.person_add_outlined, size: 100 * context.sf, color: const Color(0xFFE74C3C)),
SizedBox(height: 10 * context.sf),
Icon(
Icons.person_add_outlined,
size: logoSize,
color: const Color(0xFFE74C3C)
),
const SizedBox(height: 10),
Text(
'Nova Conta',
style: TextStyle(fontSize: 36 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[900]),
style: TextStyle(
fontSize: titleFontSize,
fontWeight: FontWeight.bold,
color: Colors.grey[900],
),
),
SizedBox(height: 5 * context.sf),
const SizedBox(height: 5),
Text(
'Cria o teu perfil no BasketTrack',
style: TextStyle(fontSize: 16 * context.sf, color: Colors.grey[600], fontWeight: FontWeight.w500),
style: TextStyle(
fontSize: subtitleFontSize,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
@@ -28,6 +45,7 @@ class RegisterHeader extends StatelessWidget {
class RegisterFormFields extends StatefulWidget {
final RegisterController controller;
const RegisterFormFields({super.key, required this.controller});
@override
@@ -39,68 +57,69 @@ class _RegisterFormFieldsState extends State<RegisterFormFields> {
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final verticalPadding = screenWidth > 600 ? 22.0 : 16.0;
// IMPORTANTE: Envolvemos tudo num Form usando a chave do controller
return Form(
key: widget.controller.formKey,
child: Column(
children: [
// Campo Nome (Opcional, mas útil)
TextFormField(
controller: widget.controller.nameController,
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'Nome Completo',
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.person_outline, size: 22 * context.sf),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
prefixIcon: const Icon(Icons.person_outline),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16),
),
),
SizedBox(height: 20 * context.sf),
const SizedBox(height: 20),
// Campo Email
TextFormField(
controller: widget.controller.emailController,
// Validação automática ligada ao controller
validator: widget.controller.validateEmail,
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'E-mail',
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),
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16),
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 20 * context.sf),
const SizedBox(height: 20),
// Campo Password
TextFormField(
controller: widget.controller.passwordController,
obscureText: _obscurePassword,
validator: widget.controller.validatePassword,
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf),
prefixIcon: const Icon(Icons.lock_outlined),
suffixIcon: IconButton(
icon: Icon(_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 22 * context.sf),
icon: Icon(_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16),
),
),
SizedBox(height: 20 * context.sf),
const SizedBox(height: 20),
// Campo Confirmar Password
TextFormField(
controller: widget.controller.confirmPasswordController,
obscureText: _obscurePassword,
validator: widget.controller.validateConfirmPassword,
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'Confirmar Palavra-passe',
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),
prefixIcon: const Icon(Icons.lock_clock_outlined),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16),
),
),
],
@@ -111,27 +130,49 @@ class _RegisterFormFieldsState extends State<RegisterFormFields> {
class RegisterButton extends StatelessWidget {
final RegisterController controller;
const RegisterButton({super.key, required this.controller});
const RegisterButton({
super.key,
required this.controller,
});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final buttonHeight = screenWidth > 600 ? 70.0 : 58.0;
final fontSize = screenWidth > 600 ? 22.0 : 18.0;
return SizedBox(
width: double.infinity,
height: 58 * context.sf,
height: buttonHeight,
child: ElevatedButton(
// Passamos o context para o controller lidar com as SnackBars e Navegação
onPressed: controller.isLoading ? null : () => controller.signUp(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 3,
),
child: controller.isLoading
? SizedBox(
width: 28 * context.sf, height: 28 * context.sf,
child: const CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(Colors.white)),
? const SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
: Text(
'Criar Conta',
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w700,
),
),
),
);
}

View File

@@ -7,14 +7,12 @@ class TeamCard extends StatelessWidget {
final Team team;
final TeamController controller;
final VoidCallback onFavoriteTap;
final double sf; // <-- Variável de escala
const TeamCard({
super.key,
required this.team,
required this.controller,
required this.onFavoriteTap,
required this.sf,
});
@override
@@ -22,17 +20,17 @@ class TeamCard extends StatelessWidget {
return Card(
color: Colors.white,
elevation: 3,
margin: EdgeInsets.only(bottom: 12 * sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)),
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
// --- 1. IMAGEM + FAVORITO ---
leading: Stack(
clipBehavior: Clip.none,
children: [
CircleAvatar(
radius: 28 * sf,
radius: 28,
backgroundColor: Colors.grey[200],
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
? NetworkImage(team.imageUrl)
@@ -40,22 +38,22 @@ class TeamCard extends StatelessWidget {
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
? Text(
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
style: TextStyle(fontSize: 24 * sf),
style: const TextStyle(fontSize: 24),
)
: null,
),
Positioned(
left: -15 * sf,
top: -10 * sf,
left: -15,
top: -10,
child: IconButton(
icon: Icon(
team.isFavorite ? Icons.star : Icons.star_border,
color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1),
size: 28 * sf,
size: 28,
shadows: [
Shadow(
color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1),
blurRadius: 4 * sf,
blurRadius: 4,
),
],
),
@@ -68,91 +66,89 @@ class TeamCard extends StatelessWidget {
// --- 2. TÍTULO ---
title: Text(
team.name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf),
overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
// --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) ---
// --- 3. SUBTÍTULO (Contagem + Época) ---
subtitle: Padding(
padding: EdgeInsets.only(top: 6.0 * sf),
padding: const EdgeInsets.only(top: 6.0),
child: Row(
children: [
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),
const Icon(Icons.groups_outlined, size: 16, color: Colors.grey),
const SizedBox(width: 4),
FutureBuilder<int>(
future: controller.getPlayerCount(team.id),
initialData: 0,
builder: (context, snapshot) {
final count = snapshot.data ?? 0;
return Text(
"$count Jogs.", // Abreviado para poupar espaço
"$count Jogadores",
style: TextStyle(
color: count > 0 ? Colors.green[700] : Colors.orange,
fontWeight: FontWeight.bold,
fontSize: 13 * sf,
fontSize: 13,
),
);
},
),
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 * sf),
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 10),
Text(
"| ${team.season}",
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
],
),
),
// --- 4. BOTÕES (Estatísticas e Apagar) ---
trailing: Row(
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 * 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 * sf),
onPressed: () => _confirmDelete(context),
),
],
trailing: SizedBox(
width: 96, // Aumentei um pouco para caberem bem os dois botões
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
tooltip: 'Ver Estatísticas',
icon: const Icon(Icons.bar_chart_rounded, color: Colors.blue),
onPressed: () {
// CORRIGIDO: Agora chama a classe TeamStatsPage corretamente
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TeamStatsPage(team: team),
),
);
},
),
IconButton(
tooltip: 'Eliminar Equipa',
icon: const Icon(Icons.delete_outline, color: Color(0xFFE74C3C)),
onPressed: () => _confirmDelete(context),
),
],
),
),
),
);
}
// Função de confirmação de exclusão
void _confirmDelete(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
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)),
title: const Text('Eliminar Equipa?'),
content: Text('Tens a certeza que queres eliminar "${team.name}"?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf)),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () {
controller.deleteTeam(team.id);
Navigator.pop(context);
},
child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * sf)),
child: const Text('Eliminar', style: TextStyle(color: Colors.red)),
),
],
),
@@ -163,9 +159,8 @@ class TeamCard extends StatelessWidget {
// --- DIALOG DE CRIAÇÃO ---
class CreateTeamDialog extends StatefulWidget {
final Function(String name, String season, String imageUrl) onConfirm;
final double sf; // Recebe a escala
const CreateTeamDialog({super.key, required this.onConfirm, required this.sf});
const CreateTeamDialog({super.key, required this.onConfirm});
@override
State<CreateTeamDialog> createState() => _CreateTeamDialogState();
@@ -179,58 +174,40 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold)),
title: const Text('Nova Equipa'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _nameController,
style: TextStyle(fontSize: 14 * widget.sf),
decoration: InputDecoration(
labelText: 'Nome da Equipa',
labelStyle: TextStyle(fontSize: 14 * widget.sf)
),
decoration: const InputDecoration(labelText: 'Nome da Equipa'),
textCapitalization: TextCapitalization.words,
),
SizedBox(height: 15 * widget.sf),
const SizedBox(height: 15),
DropdownButtonFormField<String>(
value: _selectedSeason,
decoration: InputDecoration(
labelText: 'Temporada',
labelStyle: TextStyle(fontSize: 14 * widget.sf)
),
style: TextStyle(fontSize: 14 * widget.sf, color: Colors.black87),
decoration: const InputDecoration(labelText: 'Temporada'),
items: ['2023/24', '2024/25', '2025/26']
.map((s) => DropdownMenuItem(value: s, child: Text(s)))
.toList(),
onChanged: (val) => setState(() => _selectedSeason = val!),
),
SizedBox(height: 15 * widget.sf),
const SizedBox(height: 15),
TextField(
controller: _imageController,
style: TextStyle(fontSize: 14 * widget.sf),
decoration: InputDecoration(
decoration: const InputDecoration(
labelText: 'URL Imagem ou Emoji',
labelStyle: TextStyle(fontSize: 14 * widget.sf),
hintText: 'Ex: 🏀 ou https://...',
hintStyle: TextStyle(fontSize: 14 * widget.sf)
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf))
),
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancelar')),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C),
padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)
),
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C)),
onPressed: () {
if (_nameController.text.trim().isNotEmpty) {
widget.onConfirm(
@@ -241,7 +218,7 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
Navigator.pop(context);
}
},
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)),
child: const Text('Criar', style: TextStyle(color: Colors.white)),
),
],
);

View File

@@ -58,7 +58,6 @@ flutter:
assets:
- assets/playmaker-logo.png
- assets/campo.png
- assets/playmaker-logos.png
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images