Compare commits

...

6 Commits

Author SHA1 Message Date
2a987e517b melhorar o calor 2026-03-17 10:35:38 +00:00
ec5bdc4867 git lixo 2 2026-03-16 23:25:48 +00:00
a4ef651d64 Conflitos resolvidos: mantido o código local 2026-03-16 22:28:41 +00:00
cf0a9a9890 vai te lixar github 2026-03-16 22:09:01 +00:00
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
34 changed files with 3661 additions and 1662 deletions

View File

@@ -9,7 +9,7 @@ android {
namespace = "com.example.playmaker" namespace = "com.example.playmaker"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
//ndkVersion = flutter.ndkVersion //ndkVersion = flutter.ndkVersion
ndkVersion = "27.0.12077973" ndkVersion = "26.1.10909125"
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = 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;
}
}

115
lib/classe/theme.dart Normal file
View File

@@ -0,0 +1,115 @@
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,59 @@ import '../models/game_model.dart';
class GameController { class GameController {
final _supabase = Supabase.instance.client; final _supabase = Supabase.instance.client;
// 1. LER JOGOS (Com Filtros Opcionais) // 1. LER JOGOS (Stream em Tempo Real)
Stream<List<Game>> getFilteredGames({String? teamFilter, String? seasonFilter}) { Stream<List<Game>> get gamesStream {
return _supabase
.from('games') // 1. Fica à escuta da tabela original (Garante o Tempo Real!)
.stream(primaryKey: ['id'])
.asyncMap((event) async {
// 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();
});
}
// =========================================================================
// 👇 NOVO: LER JOGOS COM FILTROS DE EQUIPA E TEMPORADA (MANTÉM OS LOGOS)
// =========================================================================
// =========================================================================
// 👇 LER JOGOS COM FILTROS DE EQUIPA E TEMPORADA (SEM ERROS DE QUERY)
// =========================================================================
Stream<List<Game>> getFilteredGames({required String teamFilter, required String seasonFilter}) {
return _supabase return _supabase
.from('games') .from('games')
.stream(primaryKey: ['id']) .stream(primaryKey: ['id'])
.asyncMap((event) async { .asyncMap((event) async {
// 👇 A CORREÇÃO ESTÁ AQUI: Lê diretamente da tabela 'games' // 1. Começamos a query APENAS com o select (Sem o order ainda!)
var query = _supabase.from('games').select(); var query = _supabase.from('games_with_logos').select();
// Aplica o filtro de Temporada // 2. Se a temporada não for "Todas", aplicamos o filtro AQUI
if (seasonFilter != null && seasonFilter.isNotEmpty && seasonFilter != 'Todas') { if (seasonFilter != 'Todas') {
query = query.eq('season', seasonFilter); query = query.eq('season', seasonFilter);
} }
// Aplica o filtro de Equipa (Procura em casa ou fora) // 3. Executamos a query e aplicamos o ORDER BY no final
if (teamFilter != null && teamFilter.isNotEmpty && teamFilter != 'Todas') { final viewData = await query.order('game_date', ascending: false);
query = query.or('my_team.eq.$teamFilter,opponent_team.eq.$teamFilter');
List<Game> games = viewData.map((json) => Game.fromMap(json)).toList();
// 4. Filtramos a equipa em memória
if (teamFilter != 'Todas') {
games = games.where((g) => g.myTeam == teamFilter || g.opponentTeam == teamFilter).toList();
} }
// Executa a query com a ordenação por data return games;
final viewData = await query.order('game_date', ascending: false);
return viewData.map((json) => Game.fromMap(json)).toList();
}); });
} }
// 2. CRIAR JOGO // 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 { Future<String?> createGame(String myTeam, String opponent, String season) async {
try { try {
final response = await _supabase.from('games').insert({ final response = await _supabase.from('games').insert({
@@ -40,16 +65,18 @@ class GameController {
'season': season, 'season': season,
'my_score': 0, 'my_score': 0,
'opponent_score': 0, 'opponent_score': 0,
'status': 'Decorrer', 'status': 'Decorrer', // Começa como "Decorrer"
'game_date': DateTime.now().toIso8601String(), '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) { } catch (e) {
print("Erro ao criar jogo: $e"); print("Erro ao criar jogo: $e");
return null; return null;
} }
} }
void dispose() {} void dispose() {
// Não é necessário fechar streams do Supabase manualmente aqui
}
} }

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class HomeController extends ChangeNotifier { class HomeController extends ChangeNotifier {
// Se precisar de estado para a home screen
int _selectedCardIndex = 0; int _selectedCardIndex = 0;
int get selectedCardIndex => _selectedCardIndex; int get selectedCardIndex => _selectedCardIndex;
@@ -11,10 +10,8 @@ class HomeController extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// Métodos adicionais para lógica da home
void navigateToDetails(String playerName) { void navigateToDetails(String playerName) {
print('Navegando para detalhes de $playerName'); print('Navegando para detalhes de $playerName');
// Implementar navegação
} }
void refreshData() { void refreshData() {

View File

@@ -8,12 +8,16 @@ class ShotRecord {
final double relativeY; final double relativeY;
final bool isMake; final bool isMake;
final String playerName; final String playerName;
final String? zone;
final int? points;
ShotRecord({ ShotRecord({
required this.relativeX, required this.relativeX,
required this.relativeY, required this.relativeY,
required this.isMake, required this.isMake,
required this.playerName required this.playerName,
this.zone,
this.points,
}); });
} }
@@ -67,7 +71,6 @@ class PlacarController {
Timer? timer; Timer? timer;
bool isRunning = false; bool isRunning = false;
// 👇 VARIÁVEIS DE CALIBRAÇÃO DO CAMPO (OS TEUS NÚMEROS!) 👇
bool isCalibrating = false; bool isCalibrating = false;
double hoopBaseX = 0.088; double hoopBaseX = 0.088;
double arcRadius = 0.459; double arcRadius = 0.459;
@@ -85,6 +88,7 @@ class PlacarController {
playerStats.clear(); playerStats.clear();
playerNumbers.clear(); playerNumbers.clear();
playerDbIds.clear(); playerDbIds.clear();
matchShots.clear(); // Limpa as bolas do último jogo
myFouls = 0; myFouls = 0;
opponentFouls = 0; opponentFouls = 0;
@@ -154,6 +158,19 @@ class PlacarController {
} }
_padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false); _padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false);
// 👇 CARREGA AS BOLINHAS ANTIGAS (MAPA DE CALOR DO JOGO ATUAL) 👇
final shotsResponse = await supabase.from('shot_locations').select().eq('game_id', gameId);
for (var shotData in shotsResponse) {
matchShots.add(ShotRecord(
relativeX: double.parse(shotData['relative_x'].toString()),
relativeY: double.parse(shotData['relative_y'].toString()),
isMake: shotData['is_make'] == true,
playerName: shotData['player_name'].toString(),
zone: shotData['zone']?.toString(),
points: shotData['points'] != null ? int.parse(shotData['points'].toString()) : null,
));
}
isLoading = false; isLoading = false;
onUpdate(); onUpdate();
} catch (e) { } catch (e) {
@@ -274,31 +291,70 @@ class PlacarController {
} }
// ========================================================================= // =========================================================================
// 👇 A MÁGICA DOS PONTOS ACONTECE AQUI 👇 // 👇 REGISTA PONTOS VINDO DO POP-UP AMARELO (E MARCA A BOLINHA)
// ========================================================================= // =========================================================================
void registerShotFromPopup(BuildContext context, String action, String targetPlayer, String zone, int points, double relativeX, double relativeY) {
// 💡 AVISO AMIGÁVEL REMOVIDO. Agora podes marcar pontos mesmo com o tempo parado!
String name = targetPlayer.replaceAll("player_my_", "").replaceAll("player_opp_", "");
bool isMyTeam = targetPlayer.startsWith("player_my_");
bool isMake = action.startsWith("add_");
// 1. ATUALIZA A ESTATÍSTICA DO JOGADOR
if (playerStats.containsKey(name)) {
playerStats[name]!['fga'] = playerStats[name]!['fga']! + 1;
if (isMake) {
playerStats[name]!['fgm'] = playerStats[name]!['fgm']! + 1;
playerStats[name]!['pts'] = playerStats[name]!['pts']! + points;
// 2. ATUALIZA O PLACAR DA EQUIPA
if (isMyTeam) {
myScore += points;
} else {
opponentScore += points;
}
}
}
// 3. CRIA A BOLINHA PARA APARECER NO CAMPO
matchShots.add(ShotRecord(
relativeX: relativeX,
relativeY: relativeY,
isMake: isMake,
playerName: name,
zone: zone,
points: points,
));
// 4. MANDA UMA MENSAGEM NO ECRÃ
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isMake ? '🔥 $name MARCOU de $zone!' : '$name FALHOU de $zone!'),
backgroundColor: isMake ? Colors.green : Colors.red,
duration: const Duration(seconds: 2),
)
);
// 5. ATUALIZA O ECRÃ
onUpdate();
}
// MANTIDO PARA CASO USES A MARCAÇÃO CLÁSSICA DIRETAMENTE NO CAMPO ESCURO
void registerShotLocation(BuildContext context, Offset position, Size size) { void registerShotLocation(BuildContext context, Offset position, Size size) {
if (pendingAction == null || pendingPlayer == null) return; if (pendingAction == null || pendingPlayer == null) return;
bool is3Pt = pendingAction!.contains("_3"); bool is3Pt = pendingAction!.contains("_3");
bool is2Pt = pendingAction!.contains("_2"); bool is2Pt = pendingAction!.contains("_2");
// O ÁRBITRO MATEMÁTICO COM AS TUAS VARIÁVEIS CALIBRADAS
if (is3Pt || is2Pt) { if (is3Pt || is2Pt) {
bool isValid = _validateShotZone(position, size, is3Pt); bool isValid = _validateShotZone(position, size, is3Pt);
if (!isValid) return;
// SE A JOGADA FOI NO SÍTIO ERRADO
if (!isValid) {
return; // <-- ESTE RETURN BLOQUEIA A GRAVAÇÃO DO PONTO!
}
} }
// SE A JOGADA FOI VÁLIDA:
bool isMake = pendingAction!.startsWith("add_pts_"); bool isMake = pendingAction!.startsWith("add_pts_");
double relX = position.dx / size.width; double relX = position.dx / size.width;
double relY = position.dy / size.height; double relY = position.dy / size.height;
String name = pendingPlayer!.replaceAll("player_my_", "").replaceAll("player_opp_", ""); String name = pendingPlayer!.replaceAll("player_my_", "").replaceAll("player_opp_", "");
matchShots.add(ShotRecord( matchShots.add(ShotRecord(
@@ -329,13 +385,10 @@ class PlacarController {
bool isInside2Pts; bool isInside2Pts;
// Lógica das laterais (Cantos)
if (distFromCenterY > cornerY) { if (distFromCenterY > cornerY) {
double distToBaseline = isLeftHalf ? relX : (1.0 - relX); double distToBaseline = isLeftHalf ? relX : (1.0 - relX);
isInside2Pts = distToBaseline <= hoopBaseX; isInside2Pts = distToBaseline <= hoopBaseX;
} } else {
// Lógica da Curva Frontal
else {
double dx = (relX - hoopX) * aspectRatio; double dx = (relX - hoopX) * aspectRatio;
double dy = (relY - hoopY); double dy = (relY - hoopY);
double distanceToHoop = math.sqrt((dx * dx) + (dy * dy)); double distanceToHoop = math.sqrt((dx * dx) + (dy * dy));
@@ -343,9 +396,8 @@ class PlacarController {
} }
if (is3Pt) return !isInside2Pts; if (is3Pt) return !isInside2Pts;
return isInside2Pts; return isInside2Pts;
} }
// 👆 ===================================================================== 👆
void cancelShotLocation() { void cancelShotLocation() {
isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate(); isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate();
@@ -493,8 +545,32 @@ class PlacarController {
await supabase.from('player_stats').insert(batchStats); await supabase.from('player_stats').insert(batchStats);
} }
// 👇 6. GUARDA AS BOLINHAS (MAPA DE CALOR) NO SUPABASE 👇
List<Map<String, dynamic>> batchShots = [];
for (var shot in matchShots) {
String? memberDbId = playerDbIds[shot.playerName];
if (memberDbId != null) {
batchShots.add({
'game_id': gameId,
'member_id': memberDbId,
'player_name': shot.playerName,
'relative_x': shot.relativeX,
'relative_y': shot.relativeY,
'is_make': shot.isMake,
'zone': shot.zone ?? 'Desconhecida',
'points': shot.points ?? (shot.isMake ? 2 : 0),
});
}
}
// Apaga os antigos (para não duplicar) e guarda os novos!
await supabase.from('shot_locations').delete().eq('game_id', gameId);
if (batchShots.isNotEmpty) {
await supabase.from('shot_locations').insert(batchShots);
}
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas e Resultados guardados com Sucesso!'), backgroundColor: Colors.green)); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas, Mapa de Calor e Resultados guardados com Sucesso!'), backgroundColor: Colors.green));
} }
} catch (e) { } 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,19 @@
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
class TeamController { class TeamController {
// Instância do cliente Supabase
final _supabase = Supabase.instance.client; final _supabase = Supabase.instance.client;
// 1. Variável fixa para guardar o Stream principal // 1. STREAM (Realtime)
late final Stream<List<Map<String, dynamic>>> teamsStream; Stream<List<Map<String, dynamic>>> get teamsStream {
return _supabase
// 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
.from('teams') .from('teams')
.stream(primaryKey: ['id']) .stream(primaryKey: ['id'])
.order('name', ascending: true) .order('name', ascending: true)
.map((data) => List<Map<String, dynamic>>.from(data)); .map((data) => List<Map<String, dynamic>>.from(data));
} }
// CRIAR // 2. CRIAR
Future<void> createTeam(String name, String season, String? imageUrl) async { Future<void> createTeam(String name, String season, String? imageUrl) async {
try { try {
await _supabase.from('teams').insert({ await _supabase.from('teams').insert({
@@ -33,50 +28,52 @@ class TeamController {
} }
} }
// ELIMINAR // 3. ELIMINAR
Future<void> deleteTeam(String id) async { Future<void> deleteTeam(String id) async {
try { try {
await _supabase.from('teams').delete().eq('id', id); await _supabase.from('teams').delete().eq('id', id);
// Limpa o cache deste teamId se a equipa for apagada
_playerCountStreams.remove(id);
} catch (e) { } catch (e) {
print("❌ Erro ao eliminar: $e"); print("❌ Erro ao eliminar: $e");
} }
} }
// FAVORITAR // 4. FAVORITAR
Future<void> toggleFavorite(String teamId, bool currentStatus) async { Future<void> toggleFavorite(String teamId, bool currentStatus) async {
try { try {
await _supabase await _supabase
.from('teams') .from('teams')
.update({'is_favorite': !currentStatus}) .update({'is_favorite': !currentStatus}) // Inverte o valor
.eq('id', teamId); .eq('id', teamId);
} catch (e) { } catch (e) {
print("❌ Erro ao favoritar: $e"); print("❌ Erro ao favoritar: $e");
} }
} }
// CONTAR JOGADORES (AGORA COM CACHE DE MEMÓRIA!) // 5. CONTAR JOGADORES (LEITURA ÚNICA)
Stream<int> getPlayerCountStream(String teamId) { Future<int> getPlayerCount(String teamId) async {
// Se já criámos um "Tubo de ligação" para esta equipa, REUTILIZA-O! try {
if (_playerCountStreams.containsKey(teamId)) { final count = await _supabase
return _playerCountStreams[teamId]!; .from('members')
.count()
.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 // 👇 6. A FUNÇÃO QUE RESOLVE O ERRO (EM TEMPO REAL) 👇
final newStream = _supabase Stream<int> getPlayerCountStream(String teamId) {
return _supabase
.from('members') .from('members')
.stream(primaryKey: ['id']) .stream(primaryKey: ['id'])
.eq('team_id', teamId) .eq('team_id', teamId)
.map((data) => data.length); .map((membros) => membros
.where((membro) => membro['type'] == 'Jogador')
_playerCountStreams[teamId] = newStream; // Guarda no dicionário .length);
return newStream;
} }
// LIMPEZA FINAL QUANDO SAÍMOS DA PÁGINA // Mantemos o dispose vazio para não quebrar a chamada na TeamsPage
void dispose() { void dispose() {}
// Limpamos o dicionário de streams para libertar memória RAM
_playerCountStreams.clear();
}
} }

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../dados_grafico.dart'; // Ajusta o caminho se der erro de import import '../dados_grafico.dart';
class PieChartController extends ChangeNotifier { class PieChartController extends ChangeNotifier {
PieChartData _chartData = const PieChartData(victories: 0, defeats: 0, draws: 0); PieChartData _chartData = const PieChartData(victories: 0, defeats: 0, draws: 0);
@@ -10,7 +10,7 @@ class PieChartController extends ChangeNotifier {
_chartData = PieChartData( _chartData = PieChartData(
victories: victories ?? _chartData.victories, victories: victories ?? _chartData.victories,
defeats: defeats ?? _chartData.defeats, defeats: defeats ?? _chartData.defeats,
draws: draws ?? _chartData.draws, // 👇 AGORA ELE ACEITA OS EMPATES draws: draws ?? _chartData.draws,
); );
notifyListeners(); notifyListeners();
} }

View File

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

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart'; import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart';
import 'dados_grafico.dart'; import 'dados_grafico.dart';
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA ADICIONADO PARA USARMOS O primaryRed
import 'dart:math' as math;
class PieChartCard extends StatefulWidget { class PieChartCard extends StatefulWidget {
final int victories; final int victories;
@@ -8,7 +10,7 @@ class PieChartCard extends StatefulWidget {
final int draws; final int draws;
final String title; final String title;
final String subtitle; final String subtitle;
final Color backgroundColor; final Color? backgroundColor;
final VoidCallback? onTap; final VoidCallback? onTap;
final double sf; final double sf;
@@ -20,7 +22,7 @@ class PieChartCard extends StatefulWidget {
this.title = 'DESEMPENHO', this.title = 'DESEMPENHO',
this.subtitle = 'Temporada', this.subtitle = 'Temporada',
this.onTap, this.onTap,
required this.backgroundColor, this.backgroundColor,
this.sf = 1.0, this.sf = 1.0,
}); });
@@ -59,30 +61,31 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
Widget build(BuildContext context) { Widget build(BuildContext context) {
final data = PieChartData(victories: widget.victories, defeats: widget.defeats, draws: widget.draws); final data = PieChartData(victories: widget.victories, defeats: widget.defeats, draws: widget.draws);
return AnimatedBuilder( // 👇 BLINDAGEM DO FUNDO E DO TEXTO PARA MODO CLARO/ESCURO
final Color cardColor = widget.backgroundColor ?? Theme.of(context).cardTheme.color ?? (Theme.of(context).brightness == Brightness.dark ? const Color(0xFF1E1E1E) : Colors.white);
final Color textColor = Theme.of(context).colorScheme.onSurface;
return AnimatedBuilder(
animation: _animation, animation: _animation,
builder: (context, child) { builder: (context, child) {
return Transform.scale( return Transform.scale(
// O scale pode passar de 1.0 (efeito back), mas a opacidade NÃO
scale: 0.95 + (_animation.value * 0.05), scale: 0.95 + (_animation.value * 0.05),
child: Opacity( child: Opacity(opacity: _animation.value.clamp(0.0, 1.0), child: child),
// 👇 AQUI ESTÁ A FIX: Garante que fica entre 0 e 1
opacity: _animation.value.clamp(0.0, 1.0),
child: child,
),
); );
}, },
child: Card( child: Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
elevation: 4, elevation: 0, // Ajustado para não ter sombra dupla, já que o tema pode ter
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: Colors.grey.withOpacity(0.15)), // Borda suave igual ao resto da app
),
child: InkWell( child: InkWell(
onTap: widget.onTap, onTap: widget.onTap,
borderRadius: BorderRadius.circular(14),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14), color: cardColor, // 👇 APLICA A COR BLINDADA
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [widget.backgroundColor.withOpacity(0.9), widget.backgroundColor.withOpacity(0.7)]),
), ),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
@@ -90,86 +93,101 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
final double cw = constraints.maxWidth; final double cw = constraints.maxWidth;
return Padding( return Padding(
padding: EdgeInsets.all(cw * 0.06), padding: EdgeInsets.symmetric(horizontal: cw * 0.05, vertical: ch * 0.03),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 👇 TÍTULOS UM POUCO MAIS PRESENTES // --- CABEÇALHO --- (👇 MANTIDO ALINHADO À ESQUERDA)
FittedBox( FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Text(widget.title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), letterSpacing: 1.0)), child: Text(widget.title.toUpperCase(),
style: TextStyle(
fontSize: ch * 0.045,
fontWeight: FontWeight.bold,
color: AppTheme.primaryRed, // 👇 USANDO O TEU primaryRed
letterSpacing: 1.2
)
),
), ),
FittedBox( Text(widget.subtitle,
fit: BoxFit.scaleDown, style: TextStyle(
child: Text(widget.subtitle, style: TextStyle(fontSize: ch * 0.07, fontWeight: FontWeight.bold, color: Colors.white)), fontSize: ch * 0.055,
fontWeight: FontWeight.bold,
color: AppTheme.backgroundLight, // 👇 USANDO O TEU backgroundLight
)
), ),
SizedBox(height: ch * 0.03), const Expanded(flex: 1, child: SizedBox()),
// MEIO (GRÁFICO + ESTATÍSTICAS) // --- MIOLO (GRÁFICO MAIOR À ESQUERDA + STATS) ---
Expanded( Expanded(
flex: 9,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.end, // Changed from spaceBetween to end to push stats more to the right
children: [ children: [
Expanded( // 1. Lado Esquerdo: Donut Chart
flex: 1, // 👇 MUDANÇA AQUI: Gráfico ainda maior! cw * 0.52
SizedBox(
width: cw * 0.52,
height: cw * 0.52,
child: PieChartWidget( child: PieChartWidget(
victoryPercentage: data.victoryPercentage, victoryPercentage: data.victoryPercentage,
defeatPercentage: data.defeatPercentage, defeatPercentage: data.defeatPercentage,
drawPercentage: data.drawPercentage, drawPercentage: data.drawPercentage,
sf: widget.sf, sf: widget.sf,
), ),
), ),
SizedBox(width: cw * 0.05),
SizedBox(width: cw * 0.005), // Reduzi o espaço no meio para dar lugar ao gráfico
// 2. Lado Direito: Números Dinâmicos
Expanded( Expanded(
flex: 1, child: FittedBox(
child: Column( alignment: Alignment.centerRight, // Encosta os números à direita
mainAxisAlignment: MainAxisAlignment.start, fit: BoxFit.scaleDown,
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
_buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, ch), crossAxisAlignment: CrossAxisAlignment.end, // Alinha os números à direita para ficar arrumado
_buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.yellow, ch), children: [
_buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, ch), _buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, textColor, ch, cw),
_buildDynDivider(ch), _buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.amber, textColor, ch, cw),
_buildDynStatRow("TOT", data.total.toString(), "100", Colors.white, ch), _buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, textColor, ch, cw),
], _buildDynDivider(cw, textColor),
_buildDynStatRow("TOT", data.total.toString(), "100", textColor, textColor, ch, cw),
],
),
), ),
), ),
], ],
), ),
), ),
// 👇 RODAPÉ AJUSTADO const Expanded(flex: 1, child: SizedBox()),
SizedBox(height: ch * 0.03),
// --- RODAPÉ: BOTÃO WIN RATE GIGANTE --- (👇 MUDANÇA AQUI: Alinhado à esquerda)
Container( Container(
width: double.infinity, width: double.infinity,
padding: EdgeInsets.symmetric(vertical: ch * 0.035), padding: EdgeInsets.symmetric(vertical: ch * 0.025),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white24, // Igual ao fundo do botão detalhes color: textColor.withOpacity(0.05), // 👇 Fundo adaptável
borderRadius: BorderRadius.circular(ch * 0.03), // Borda arredondada borderRadius: BorderRadius.circular(12),
), ),
child: Center( child: FittedBox(
child: FittedBox( fit: BoxFit.scaleDown,
fit: BoxFit.scaleDown, child: Row(
child: Row( mainAxisAlignment: MainAxisAlignment.start, // 👇 MUDANÇA AQUI: Letras mais para a esquerda!
mainAxisAlignment: MainAxisAlignment.center, children: [
children: [ Icon(Icons.stars, color: Colors.green, size: ch * 0.075),
Icon( const SizedBox(width: 10),
data.victoryPercentage >= 0.5 ? Icons.trending_up : Icons.trending_down, Text('WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
color: Colors.green, style: TextStyle(
size: ch * 0.09 color: AppTheme.backgroundLight,
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
fontSize: ch * 0.06
), ),
SizedBox(width: cw * 0.02), ),
Text( ],
'WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: ch * 0.05,
fontWeight: FontWeight.bold,
color: Colors.white
)
),
],
),
), ),
), ),
), ),
@@ -183,34 +201,39 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
), ),
); );
} }
// 👇 PERCENTAGENS SUBIDAS LIGEIRAMENTE (0.10 e 0.045)
Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch) { // 👇 Ajustei a linha de stats para alinhar melhor agora que os números estão encostados à direita
Widget _buildDynStatRow(String label, String number, String percent, Color statColor, Color textColor, double ch, double cw) {
return Padding( return Padding(
padding: EdgeInsets.only(bottom: ch * 0.01), padding: EdgeInsets.symmetric(vertical: ch * 0.005),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Número subiu para 0.10 SizedBox(
Expanded(flex: 2, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(number, style: TextStyle(fontSize: ch * 0.10, fontWeight: FontWeight.bold, color: color, height: 1.0)))), width: cw * 0.12,
SizedBox(width: ch * 0.02), child: Column(
Expanded( crossAxisAlignment: CrossAxisAlignment.end,
flex: 3, mainAxisSize: MainAxisSize.min,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ children: [
Row(children: [ Text(label, style: TextStyle(fontSize: ch * 0.045, color: textColor.withOpacity(0.6), fontWeight: FontWeight.bold)), // 👇 TEXTO ADAPTÁVEL (increased from 0.035)
Container(width: ch * 0.018, height: ch * 0.018, margin: EdgeInsets.only(right: ch * 0.015), decoration: BoxDecoration(color: color, shape: BoxShape.circle)), Text('$percent%', style: TextStyle(fontSize: ch * 0.05, color: statColor, fontWeight: FontWeight.bold)), // (increased from 0.04)
// Label subiu para 0.045 ],
Expanded(child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(label, style: TextStyle(fontSize: ch * 0.033, color: Colors.white.withOpacity(0.8), fontWeight: FontWeight.w600)))) ),
]),
// Percentagem subiu para 0.05
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('$percent%', style: TextStyle(fontSize: ch * 0.04, color: color, fontWeight: FontWeight.bold))),
]),
), ),
SizedBox(width: cw * 0.03),
Text(number, style: TextStyle(fontSize: ch * 0.15, fontWeight: FontWeight.w900, color: statColor, height: 1)), // (increased from 0.125)
], ],
), ),
); );
} }
Widget _buildDynDivider(double ch) { Widget _buildDynDivider(double cw, Color textColor) {
return Container(height: 0.5, color: Colors.white.withOpacity(0.1), margin: EdgeInsets.symmetric(vertical: ch * 0.01)); return Container(
width: cw * 0.35,
height: 1.5,
color: textColor.withOpacity(0.2), // 👇 LINHA ADAPTÁVEL
margin: const EdgeInsets.symmetric(vertical: 4)
);
} }
} }

View File

@@ -19,12 +19,9 @@ class PieChartWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { 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 w = constraints.maxWidth.isInfinite ? 100.0 : constraints.maxWidth;
final double h = constraints.maxHeight.isInfinite ? 100.0 : constraints.maxHeight; 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); final double size = math.min(w, h);
return Center( return Center(
@@ -32,7 +29,7 @@ class PieChartWidget extends StatelessWidget {
width: size, width: size,
height: size, height: size,
child: CustomPaint( child: CustomPaint(
painter: _PieChartPainter( painter: _DonutChartPainter(
victoryPercentage: victoryPercentage, victoryPercentage: victoryPercentage,
defeatPercentage: defeatPercentage, defeatPercentage: defeatPercentage,
drawPercentage: drawPercentage, drawPercentage: drawPercentage,
@@ -48,24 +45,27 @@ class PieChartWidget extends StatelessWidget {
} }
Widget _buildCenterLabels(double size) { Widget _buildCenterLabels(double size) {
final bool hasGames = victoryPercentage > 0 || defeatPercentage > 0 || drawPercentage > 0;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
'${(victoryPercentage * 100).toStringAsFixed(1)}%', // 👇 Casa decimal aplicada aqui!
hasGames ? '${(victoryPercentage * 100).toStringAsFixed(1)}%' : '---',
style: TextStyle( style: TextStyle(
fontSize: size * 0.18, // O texto cresce ou encolhe com o círculo fontSize: size * (hasGames ? 0.20 : 0.15),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: hasGames ? Colors.white : Colors.white54,
), ),
), ),
SizedBox(height: size * 0.02), SizedBox(height: size * 0.02),
Text( Text(
'Vitórias', hasGames ? 'Vitórias' : 'Sem Jogos',
style: TextStyle( style: TextStyle(
fontSize: size * 0.10, fontSize: size * 0.08,
color: Colors.white.withOpacity(0.8), color: hasGames ? Colors.white70 : Colors.white38,
), ),
), ),
], ],
@@ -73,12 +73,12 @@ class PieChartWidget extends StatelessWidget {
} }
} }
class _PieChartPainter extends CustomPainter { class _DonutChartPainter extends CustomPainter {
final double victoryPercentage; final double victoryPercentage;
final double defeatPercentage; final double defeatPercentage;
final double drawPercentage; final double drawPercentage;
_PieChartPainter({ _DonutChartPainter({
required this.victoryPercentage, required this.victoryPercentage,
required this.defeatPercentage, required this.defeatPercentage,
required this.drawPercentage, required this.drawPercentage,
@@ -87,59 +87,40 @@ class _PieChartPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2); 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.1);
final radius = (size.width / 2) - (size.width * 0.05); final strokeWidth = size.width * 0.2;
if (victoryPercentage == 0 && defeatPercentage == 0 && drawPercentage == 0) {
final bgPaint = Paint()
..color = Colors.white.withOpacity(0.05)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth;
canvas.drawCircle(center, radius, bgPaint);
return;
}
const victoryColor = Colors.green; const victoryColor = Colors.green;
const defeatColor = Colors.red; const defeatColor = Colors.red;
const drawColor = Colors.yellow; const drawColor = Colors.amber;
const borderColor = Colors.white30;
double startAngle = -math.pi / 2; double startAngle = -math.pi / 2;
if (victoryPercentage > 0) {
final sweepAngle = 2 * math.pi * victoryPercentage;
_drawSector(canvas, center, radius, startAngle, sweepAngle, victoryColor, size.width);
startAngle += sweepAngle;
}
if (drawPercentage > 0) {
final sweepAngle = 2 * math.pi * drawPercentage;
_drawSector(canvas, center, radius, startAngle, sweepAngle, drawColor, size.width);
startAngle += sweepAngle;
}
if (defeatPercentage > 0) {
final sweepAngle = 2 * math.pi * defeatPercentage;
_drawSector(canvas, center, radius, startAngle, sweepAngle, defeatColor, size.width);
}
final borderPaint = Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = size.width * 0.02;
canvas.drawCircle(center, radius, borderPaint);
}
void _drawSector(Canvas canvas, Offset center, double radius, double startAngle, double sweepAngle, Color color, double totalWidth) { void drawDonutSector(double percentage, Color color) {
final paint = Paint() if (percentage <= 0) return;
..color = color final sweepAngle = 2 * math.pi * percentage;
..style = PaintingStyle.fill; final paint = Paint()
..color = color
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, true, paint);
if (sweepAngle < 2 * math.pi) {
final linePaint = Paint()
..color = Colors.white.withOpacity(0.5)
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = totalWidth * 0.015; ..strokeWidth = strokeWidth
..strokeCap = StrokeCap.butt;
final lineX = center.dx + radius * math.cos(startAngle); canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, paint);
final lineY = center.dy + radius * math.sin(startAngle); startAngle += sweepAngle;
canvas.drawLine(center, Offset(lineX, lineY), linePaint);
} }
drawDonutSector(victoryPercentage, victoryColor);
drawDonutSector(drawPercentage, drawColor);
drawDonutSector(defeatPercentage, defeatColor);
} }
@override @override

View File

@@ -1,35 +1,42 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORTA O TEU TEMA
import 'pages/login.dart'; import 'pages/login.dart';
// ========================================================
// 👇 A VARIÁVEL MÁGICA QUE FALTAVA (Fora do void main) 👇
// ========================================================
final ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize( await Supabase.initialize(
url: 'https://sihwjdshexjyvsbettcd.supabase.co', url: 'https://sihwjdshexjyvsbettcd.supabase.co',
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpaHdqZHNoZXhqeXZzYmV0dGNkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njg5MTQxMjgsImV4cCI6MjA4NDQ5MDEyOH0.gW3AvTJVNyE1Dqa72OTnhrUIKsndexrY3pKxMIAaAy8', // Uma string longa anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpaHdqZHNoZXhqeXZzYmV0dGNkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njg5MTQxMjgsImV4cCI6MjA4NDQ5MDEyOH0.gW3AvTJVNyE1Dqa72OTnhrUIKsndexrY3pKxMIAaAy8',
); );
runApp(const MyApp()); runApp(const MyApp());
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( // FICA À ESCUTA DO THEMENOTIFIER
debugShowCheckedModeBanner: false, return ValueListenableBuilder<ThemeMode>(
title: 'PlayMaker', valueListenable: themeNotifier,
theme: ThemeData( builder: (_, ThemeMode currentMode, __) {
colorScheme: ColorScheme.fromSeed( return MaterialApp(
seedColor: const Color(0xFFE74C3C), debugShowCheckedModeBanner: false,
), title: 'PlayMaker',
useMaterial3: true, theme: AppTheme.lightTheme,
), darkTheme: AppTheme.darkTheme,
home: const LoginPage(), themeMode: currentMode, // 👇 ISTO RECEBE O VALOR DO NOTIFIER
home: const LoginPage(),
);
},
); );
} }
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA
import '../controllers/register_controller.dart'; import '../controllers/register_controller.dart';
import '../widgets/register_widgets.dart'; import '../widgets/register_widgets.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER! import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
@@ -22,11 +23,20 @@ class _RegisterPageState extends State<RegisterPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, // 👇 BLINDADO: Adapta-se automaticamente ao Modo Claro/Escuro
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
title: Text("Criar Conta", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), title: Text(
backgroundColor: Colors.white, "Criar Conta",
style: TextStyle(
fontSize: 18 * context.sf,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável ao Modo Escuro
)
),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 0, elevation: 0,
iconTheme: IconThemeData(color: Theme.of(context).colorScheme.onSurface),
), ),
body: Center( body: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -40,7 +50,7 @@ class _RegisterPageState extends State<RegisterPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const RegisterHeader(), // 🔥 Agora sim, usa o Header bonito! const RegisterHeader(),
SizedBox(height: 30 * context.sf), SizedBox(height: 30 * context.sf),
RegisterFormFields(controller: _controller), RegisterFormFields(controller: _controller),

View File

@@ -1,76 +1,91 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/pages/PlacarPage.dart'; import 'package:playmaker/pages/PlacarPage.dart';
import '../controllers/game_controller.dart'; import 'package:playmaker/classe/theme.dart';
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../controllers/game_controller.dart';
import '../models/game_model.dart'; import '../models/game_model.dart';
import '../utils/size_extension.dart'; // 👇 NOVO SUPERPODER AQUI TAMBÉM! import '../utils/size_extension.dart';
// --- CARD DE EXIBIÇÃO DO JOGO --- // --- CARD DE EXIBIÇÃO DO JOGO ---
class GameResultCard extends StatelessWidget { class GameResultCard extends StatelessWidget {
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season; final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
final String? myTeamLogo, opponentTeamLogo; final String? myTeamLogo, opponentTeamLogo;
final double sf;
const GameResultCard({ const GameResultCard({
super.key, required this.gameId, required this.myTeam, required this.opponentTeam, super.key, required this.gameId, required this.myTeam, required this.opponentTeam,
required this.myScore, required this.opponentScore, required this.status, required this.season, required this.myScore, required this.opponentScore, required this.status, required this.season,
this.myTeamLogo, this.opponentTeamLogo, this.myTeamLogo, this.opponentTeamLogo, required this.sf,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bgColor = Theme.of(context).cardTheme.color;
final textColor = Theme.of(context).colorScheme.onSurface;
return Container( return Container(
margin: EdgeInsets.only(bottom: 16 * context.sf), margin: EdgeInsets.only(bottom: 16 * sf),
padding: EdgeInsets.all(16 * context.sf), padding: EdgeInsets.all(16 * sf),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * context.sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * context.sf)]), decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(20 * sf),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: _buildTeamInfo(context, myTeam, const Color(0xFFE74C3C), myTeamLogo)), Expanded(child: _buildTeamInfo(myTeam, AppTheme.primaryRed, myTeamLogo, sf, textColor)),
_buildScoreCenter(context, gameId), _buildScoreCenter(context, gameId, sf, textColor),
Expanded(child: _buildTeamInfo(context, opponentTeam, Colors.black87, opponentTeamLogo)), Expanded(child: _buildTeamInfo(opponentTeam, Colors.grey.shade600, opponentTeamLogo, sf, textColor)),
], ],
), ),
); );
} }
Widget _buildTeamInfo(BuildContext context, String name, Color color, String? logoUrl) { Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) {
return Column( return Column(
children: [ children: [
CircleAvatar(radius: 24 * context.sf, backgroundColor: color, backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * context.sf) : null), CircleAvatar(
SizedBox(height: 6 * context.sf), radius: 24 * sf,
Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2), backgroundColor: color,
backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null,
child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * sf) : null,
),
SizedBox(height: 6 * sf),
Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf, color: textColor), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2),
], ],
); );
} }
Widget _buildScoreCenter(BuildContext context, String id) { Widget _buildScoreCenter(BuildContext context, String id, double sf, Color textColor) {
return Column( return Column(
children: [ children: [
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_scoreBox(context, myScore, Colors.green), _scoreBox(myScore, AppTheme.successGreen, sf),
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * context.sf)), Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf, color: textColor)),
_scoreBox(context, opponentScore, Colors.grey), _scoreBox(opponentScore, Colors.grey, sf),
], ],
), ),
SizedBox(height: 10 * context.sf), SizedBox(height: 10 * sf),
TextButton.icon( TextButton.icon(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))),
icon: Icon(Icons.play_circle_fill, size: 18 * context.sf, color: const Color(0xFFE74C3C)), icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: AppTheme.primaryRed),
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * context.sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)), label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: AppTheme.primaryRed, fontWeight: FontWeight.bold)),
style: TextButton.styleFrom(backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), padding: EdgeInsets.symmetric(horizontal: 14 * context.sf, vertical: 8 * context.sf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), visualDensity: VisualDensity.compact), style: TextButton.styleFrom(backgroundColor: AppTheme.primaryRed.withOpacity(0.1), padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), visualDensity: VisualDensity.compact),
), ),
SizedBox(height: 6 * context.sf), SizedBox(height: 6 * sf),
Text(status, style: TextStyle(fontSize: 12 * context.sf, color: Colors.blue, fontWeight: FontWeight.bold)), Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)),
], ],
); );
} }
Widget _scoreBox(BuildContext context, String pts, Color c) => Container( Widget _scoreBox(String pts, Color c, double sf) => Container(
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf, vertical: 6 * context.sf), padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * context.sf)), decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)),
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)), child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
); );
} }
@@ -78,8 +93,9 @@ class GameResultCard extends StatelessWidget {
class CreateGameDialogManual extends StatefulWidget { class CreateGameDialogManual extends StatefulWidget {
final TeamController teamController; final TeamController teamController;
final GameController gameController; final GameController gameController;
final double sf;
const CreateGameDialogManual({super.key, required this.teamController, required this.gameController}); const CreateGameDialogManual({super.key, required this.teamController, required this.gameController, required this.sf});
@override @override
State<CreateGameDialogManual> createState() => _CreateGameDialogManualState(); State<CreateGameDialogManual> createState() => _CreateGameDialogManualState();
@@ -105,24 +121,29 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), backgroundColor: Theme.of(context).colorScheme.surface,
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * widget.sf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * widget.sf, color: Theme.of(context).colorScheme.onSurface)),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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))), TextField(
SizedBox(height: 15 * context.sf), controller: _seasonController,
style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * widget.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * widget.sf))
),
SizedBox(height: 15 * widget.sf),
_buildSearch(context, "Minha Equipa", _myTeamController), _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))), Padding(padding: EdgeInsets.symmetric(vertical: 10 * widget.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * widget.sf))),
_buildSearch(context, "Adversário", _opponentController), _buildSearch(context, "Adversário", _opponentController),
], ],
), ),
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * context.sf))), TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)), style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * widget.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)),
onPressed: _isLoading ? null : () async { onPressed: _isLoading ? null : () async {
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) { if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
setState(() => _isLoading = true); setState(() => _isLoading = true);
@@ -134,7 +155,7 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
} }
} }
}, },
child: _isLoading ? SizedBox(width: 20 * context.sf, height: 20 * context.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), child: _isLoading ? SizedBox(width: 20 * 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)),
), ),
], ],
); );
@@ -156,9 +177,10 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
return Align( return Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Material( child: Material(
elevation: 4.0, borderRadius: BorderRadius.circular(8 * context.sf), color: Theme.of(context).colorScheme.surface,
elevation: 4.0, borderRadius: BorderRadius.circular(8 * widget.sf),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 250 * context.sf, maxWidth: MediaQuery.of(context).size.width * 0.7), constraints: BoxConstraints(maxHeight: 250 * widget.sf, maxWidth: MediaQuery.of(context).size.width * 0.7),
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
@@ -166,8 +188,8 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
final String name = option['name'].toString(); final String name = option['name'].toString();
final String? imageUrl = option['image_url']; final String? imageUrl = option['image_url'];
return ListTile( return ListTile(
leading: CircleAvatar(radius: 20 * context.sf, backgroundColor: Colors.grey.shade200, backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * context.sf) : null), leading: CircleAvatar(radius: 20 * widget.sf, backgroundColor: Colors.grey.withOpacity(0.2), backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * widget.sf) : null),
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface)),
onTap: () { onSelected(option); }, onTap: () { onSelected(option); },
); );
}, },
@@ -180,8 +202,9 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) txtCtrl.text = controller.text; if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) txtCtrl.text = controller.text;
txtCtrl.addListener(() { controller.text = txtCtrl.text; }); txtCtrl.addListener(() { controller.text = txtCtrl.text; });
return TextField( return TextField(
controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * context.sf), controller: txtCtrl, focusNode: node,
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * context.sf), prefixIcon: Icon(Icons.search, size: 20 * context.sf), border: const OutlineInputBorder()), style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * widget.sf), prefixIcon: Icon(Icons.search, size: 20 * widget.sf, color: AppTheme.primaryRed)),
); );
}, },
); );
@@ -209,16 +232,16 @@ class _GamePageState extends State<GamePage> {
bool isFilterActive = selectedSeason != 'Todas' || selectedTeam != 'Todas'; bool isFilterActive = selectedSeason != 'Todas' || selectedTeam != 'Todas';
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F7FA), backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)), title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)),
backgroundColor: Colors.white, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 0, elevation: 0,
actions: [ actions: [
Padding( Padding(
padding: EdgeInsets.only(right: 8.0 * context.sf), padding: EdgeInsets.only(right: 8.0 * context.sf),
child: IconButton( child: IconButton(
icon: Icon(isFilterActive ? Icons.filter_list_alt : Icons.filter_list, color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87, size: 26 * context.sf), icon: Icon(isFilterActive ? Icons.filter_list_alt : Icons.filter_list, color: isFilterActive ? AppTheme.primaryRed : Theme.of(context).colorScheme.onSurface, size: 26 * context.sf),
onPressed: () => _showFilterPopup(context), onPressed: () => _showFilterPopup(context),
), ),
) )
@@ -232,9 +255,9 @@ class _GamePageState extends State<GamePage> {
stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason), stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason),
builder: (context, gameSnapshot) { builder: (context, gameSnapshot) {
if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) return const Center(child: CircularProgressIndicator()); 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.hasError) return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * context.sf, color: Theme.of(context).colorScheme.onSurface)));
if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) { 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))])); return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.search_off, size: 48 * context.sf, color: Colors.grey.withOpacity(0.3)), SizedBox(height: 10 * context.sf), Text("Nenhum jogo encontrado.", style: TextStyle(fontSize: 14 * context.sf, color: Colors.grey))]));
} }
return ListView.builder( return ListView.builder(
padding: EdgeInsets.all(16 * context.sf), padding: EdgeInsets.all(16 * context.sf),
@@ -249,6 +272,7 @@ class _GamePageState extends State<GamePage> {
return GameResultCard( return GameResultCard(
gameId: game.id, myTeam: game.myTeam, opponentTeam: game.opponentTeam, myScore: game.myScore, 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, opponentScore: game.opponentScore, status: game.status, season: game.season, myTeamLogo: myLogo, opponentTeamLogo: oppLogo,
sf: context.sf,
); );
}, },
); );
@@ -257,10 +281,10 @@ class _GamePageState extends State<GamePage> {
}, },
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'add_game_btn', // 👇 A MÁGICA ESTÁ AQUI TAMBÉM! heroTag: 'add_game_btn',
backgroundColor: const Color(0xFFE74C3C), backgroundColor: AppTheme.primaryRed,
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf), child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
onPressed: () => showDialog(context: context, builder: (context) => CreateGameDialogManual(teamController: teamController, gameController: gameController)), onPressed: () => showDialog(context: context, builder: (context) => CreateGameDialogManual(teamController: teamController, gameController: gameController, sf: context.sf)),
), ),
); );
} }
@@ -274,34 +298,36 @@ class _GamePageState extends State<GamePage> {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setPopupState) { builder: (context, setPopupState) {
return AlertDialog( return AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)), Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf, color: Theme.of(context).colorScheme.onSurface)),
IconButton(icon: const Icon(Icons.close, color: Colors.grey), onPressed: () => Navigator.pop(context), padding: EdgeInsets.zero, constraints: const BoxConstraints()) IconButton(icon: const Icon(Icons.close, color: Colors.grey), onPressed: () => Navigator.pop(context), padding: EdgeInsets.zero, constraints: const BoxConstraints())
], ],
), ),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text("Temporada", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), Text("Temporada", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * context.sf), SizedBox(height: 6 * context.sf),
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)), padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(10 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))),
child: DropdownButtonHideUnderline( child: DropdownButtonHideUnderline(
child: DropdownButton<String>( child: DropdownButton<String>(
isExpanded: true, value: tempSeason, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold), dropdownColor: Theme.of(context).colorScheme.surface,
isExpanded: true, value: tempSeason, style: TextStyle(fontSize: 14 * context.sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold),
items: ['Todas', '2024/25', '2025/26'].map((String value) => DropdownMenuItem<String>(value: value, child: Text(value))).toList(), items: ['Todas', '2024/25', '2025/26'].map((String value) => DropdownMenuItem<String>(value: value, child: Text(value))).toList(),
onChanged: (newValue) => setPopupState(() => tempSeason = newValue!), onChanged: (newValue) => setPopupState(() => tempSeason = newValue!),
), ),
), ),
), ),
SizedBox(height: 20 * context.sf), SizedBox(height: 20 * context.sf),
Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * context.sf), SizedBox(height: 6 * context.sf),
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)), padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(10 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))),
child: StreamBuilder<List<Map<String, dynamic>>>( child: StreamBuilder<List<Map<String, dynamic>>>(
stream: teamController.teamsStream, stream: teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -310,7 +336,8 @@ class _GamePageState extends State<GamePage> {
if (!teamNames.contains(tempTeam)) tempTeam = 'Todas'; if (!teamNames.contains(tempTeam)) tempTeam = 'Todas';
return DropdownButtonHideUnderline( return DropdownButtonHideUnderline(
child: DropdownButton<String>( child: DropdownButton<String>(
isExpanded: true, value: tempTeam, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold), dropdownColor: Theme.of(context).colorScheme.surface,
isExpanded: true, value: tempTeam, style: TextStyle(fontSize: 14 * context.sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold),
items: teamNames.map((String value) => DropdownMenuItem<String>(value: value, child: Text(value, overflow: TextOverflow.ellipsis))).toList(), items: teamNames.map((String value) => DropdownMenuItem<String>(value: value, child: Text(value, overflow: TextOverflow.ellipsis))).toList(),
onChanged: (newValue) => setPopupState(() => tempTeam = newValue!), onChanged: (newValue) => setPopupState(() => tempTeam = newValue!),
), ),
@@ -322,7 +349,7 @@ class _GamePageState extends State<GamePage> {
), ),
actions: [ actions: [
TextButton(onPressed: () { setState(() { selectedSeason = 'Todas'; selectedTeam = 'Todas'; }); Navigator.pop(context); }, child: Text('LIMPAR', style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey))), 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))), ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, 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))),
], ],
); );
} }

View File

@@ -1,13 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/classe/home.config.dart'; import 'package:playmaker/classe/theme.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/gamePage.dart';
import 'package:playmaker/pages/teamPage.dart'; import 'package:playmaker/pages/teamPage.dart';
import 'package:playmaker/controllers/team_controller.dart'; import 'package:playmaker/controllers/team_controller.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:playmaker/pages/status_page.dart'; import 'package:playmaker/pages/status_page.dart';
import '../utils/size_extension.dart'; import '../utils/size_extension.dart';
import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart'; import 'settings_screen.dart';
// 👇 Importa o ficheiro onde meteste o StatCard e o SportGrid
// import 'home_widgets.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@@ -30,24 +32,27 @@ class _HomeScreenState extends State<HomeScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Já não precisamos calcular o sf aqui!
final List<Widget> pages = [ final List<Widget> pages = [
_buildHomeContent(context), // Passamos só o context _buildHomeContent(context),
const GamePage(), const GamePage(),
const TeamsPage(), const TeamsPage(),
const StatusPage(), const StatusPage(),
]; ];
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)), title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)),
backgroundColor: HomeConfig.primaryColor, backgroundColor: AppTheme.primaryRed,
foregroundColor: Colors.white, foregroundColor: Colors.white,
leading: IconButton( leading: IconButton(
icon: Icon(Icons.person, size: 24 * context.sf), icon: Icon(Icons.person, size: 24 * context.sf),
onPressed: () {}, onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingsScreen()),
);
},
), ),
), ),
@@ -62,7 +67,6 @@ class _HomeScreenState extends State<HomeScreen> {
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
elevation: 1, elevation: 1,
// O math.min não é necessário se já tens o sf. Mas podes usar context.sf
height: 70 * (context.sf < 1.2 ? context.sf : 1.2), height: 70 * (context.sf < 1.2 ? context.sf : 1.2),
destinations: const [ destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'), NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'),
@@ -77,13 +81,14 @@ class _HomeScreenState extends State<HomeScreen> {
void _showTeamSelector(BuildContext context) { void _showTeamSelector(BuildContext context) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf))),
builder: (context) { builder: (context) {
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: _teamController.teamsStream, stream: _teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * context.sf, child: const Center(child: Text("Nenhuma equipa criada."))); if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface))));
final teams = snapshot.data!; final teams = snapshot.data!;
return ListView.builder( return ListView.builder(
@@ -92,7 +97,7 @@ class _HomeScreenState extends State<HomeScreen> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final team = teams[index]; final team = teams[index];
return ListTile( return ListTile(
title: Text(team['name']), title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
onTap: () { onTap: () {
setState(() { setState(() {
_selectedTeamId = team['id']; _selectedTeamId = team['id'];
@@ -115,6 +120,7 @@ class _HomeScreenState extends State<HomeScreen> {
Widget _buildHomeContent(BuildContext context) { Widget _buildHomeContent(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width; final double wScreen = MediaQuery.of(context).size.width;
final double cardHeight = wScreen * 0.5; final double cardHeight = wScreen * 0.5;
final textColor = Theme.of(context).colorScheme.onSurface;
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: _selectedTeamId != null stream: _selectedTeamId != null
@@ -133,12 +139,20 @@ class _HomeScreenState extends State<HomeScreen> {
onTap: () => _showTeamSelector(context), onTap: () => _showTeamSelector(context),
child: Container( child: Container(
padding: EdgeInsets.all(12 * context.sf), 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)), decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(15 * context.sf),
border: Border.all(color: Colors.grey.withOpacity(0.2))
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row(children: [Icon(Icons.shield, color: HomeConfig.primaryColor, size: 24 * context.sf), SizedBox(width: 10 * context.sf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold))]), Row(children: [
const Icon(Icons.arrow_drop_down), Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
SizedBox(width: 10 * context.sf),
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
]),
Icon(Icons.arrow_drop_down, color: textColor),
], ],
), ),
), ),
@@ -149,9 +163,9 @@ class _HomeScreenState extends State<HomeScreen> {
height: cardHeight, height: cardHeight,
child: Row( child: Row(
children: [ children: [
Expanded(child: _buildStatCard(context: context, title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)), Expanded(child: _buildStatCard(context: context, title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statPtsBg, isHighlighted: true)),
SizedBox(width: 12 * context.sf), SizedBox(width: 12 * context.sf),
Expanded(child: _buildStatCard(context: context, title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))), Expanded(child: _buildStatCard(context: context, title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statAstBg)),
], ],
), ),
), ),
@@ -161,7 +175,7 @@ class _HomeScreenState extends State<HomeScreen> {
height: cardHeight, height: cardHeight,
child: Row( child: Row(
children: [ children: [
Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))), Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statRebBg)),
SizedBox(width: 12 * context.sf), SizedBox(width: 12 * context.sf),
Expanded( Expanded(
child: PieChartCard( child: PieChartCard(
@@ -170,8 +184,8 @@ class _HomeScreenState extends State<HomeScreen> {
draws: _teamDraws, draws: _teamDraws,
title: 'DESEMPENHO', title: 'DESEMPENHO',
subtitle: 'Temporada', subtitle: 'Temporada',
backgroundColor: const Color(0xFFC62828), backgroundColor: AppTheme.statPieBg,
sf: context.sf // Aqui o PieChartCard ainda usa sf, então passamos sf: context.sf
), ),
), ),
], ],
@@ -179,20 +193,63 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
SizedBox(height: 40 * context.sf), SizedBox(height: 40 * context.sf),
Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[800])), Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: textColor)),
SizedBox(height: 16 * context.sf), SizedBox(height: 16 * context.sf),
// 👇 AQUI ESTÁ O NOVO CARTÃO VAZIO PARA QUANDO NÃO HÁ EQUIPA 👇
_selectedTeamName == "Selecionar Equipa" _selectedTeamName == "Selecionar Equipa"
? Container( ? Container(
padding: EdgeInsets.all(20 * context.sf), width: double.infinity,
alignment: Alignment.center, padding: EdgeInsets.all(24.0 * context.sf),
child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)), decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color ?? Colors.white,
borderRadius: BorderRadius.circular(16 * context.sf),
border: Border.all(color: Colors.grey.withOpacity(0.1)),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4)),
],
),
child: Column(
children: [
Container(
padding: EdgeInsets.all(18 * context.sf),
decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.08), shape: BoxShape.circle),
child: Icon(Icons.shield_outlined, color: AppTheme.primaryRed, size: 42 * context.sf),
),
SizedBox(height: 20 * context.sf),
Text(
"Nenhuma Equipa Ativa",
style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface),
),
SizedBox(height: 8 * context.sf),
Text(
"Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13 * context.sf, color: Colors.grey.shade600, height: 1.4),
),
SizedBox(height: 24 * context.sf),
SizedBox(
width: double.infinity,
height: 48 * context.sf,
child: ElevatedButton.icon(
onPressed: () => _showTeamSelector(context),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryRed,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf)),
),
icon: Icon(Icons.touch_app, size: 20 * context.sf),
label: Text("Selecionar Agora", style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.bold)),
),
),
],
),
) )
: StreamBuilder<List<Map<String, dynamic>>>( : StreamBuilder<List<Map<String, dynamic>>>(
stream: _supabase.from('games').stream(primaryKey: ['id']) stream: _supabase.from('games').stream(primaryKey: ['id'])
.order('game_date', ascending: false), .order('game_date', ascending: false),
builder: (context, gameSnapshot) { builder: (context, gameSnapshot) {
if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red)); if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
if (gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator()); if (gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
@@ -207,9 +264,9 @@ class _HomeScreenState extends State<HomeScreen> {
if (gamesList.isEmpty) { if (gamesList.isEmpty) {
return Container( return Container(
padding: EdgeInsets.all(20 * context.sf), padding: EdgeInsets.all(20 * context.sf),
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(14)),
alignment: Alignment.center, alignment: Alignment.center,
child: Text("Ainda não há jogos terminados para $_selectedTeamName.", style: TextStyle(color: Colors.grey)), child: const Text("Ainda não há jogos terminados.", style: TextStyle(color: Colors.grey)),
); );
} }
@@ -236,23 +293,15 @@ class _HomeScreenState extends State<HomeScreen> {
if (myScore < oppScore) result = 'D'; if (myScore < oppScore) result = 'D';
return _buildGameHistoryCard( return _buildGameHistoryCard(
context: context, // Usamos o context para o sf context: context, opponent: opponent, result: result,
opponent: opponent, myScore: myScore, oppScore: oppScore, date: date,
result: result, topPts: game['top_pts_name'] ?? '---', topAst: game['top_ast_name'] ?? '---',
myScore: myScore, topRbs: game['top_rbs_name'] ?? '---', topDef: game['top_def_name'] ?? '---', mvp: game['mvp_name'] ?? '---',
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(), }).toList(),
); );
}, },
), ),
SizedBox(height: 20 * context.sf), SizedBox(height: 20 * context.sf),
], ],
), ),
@@ -280,14 +329,13 @@ class _HomeScreenState extends State<HomeScreen> {
Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) { 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( return Card(
elevation: 4, margin: EdgeInsets.zero, elevation: 4, margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: AppTheme.warningAmber, width: 2) : BorderSide.none),
child: Container( child: Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])), decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final double ch = constraints.maxHeight; final double ch = constraints.maxHeight;
final double cw = constraints.maxWidth; final double cw = constraints.maxWidth;
return Padding( return Padding(
padding: EdgeInsets.all(cw * 0.06), padding: EdgeInsets.all(cw * 0.06),
child: Column( child: Column(
@@ -327,13 +375,15 @@ class _HomeScreenState extends State<HomeScreen> {
}) { }) {
bool isWin = result == 'V'; bool isWin = result == 'V';
bool isDraw = result == 'E'; bool isDraw = result == 'E';
Color statusColor = isWin ? Colors.green : (isDraw ? Colors.yellow.shade700 : Colors.red); Color statusColor = isWin ? AppTheme.successGreen : (isDraw ? AppTheme.warningAmber : AppTheme.oppTeamRed);
final bgColor = Theme.of(context).cardTheme.color;
final textColor = Theme.of(context).colorScheme.onSurface;
return Container( return Container(
margin: EdgeInsets.only(bottom: 14 * context.sf), margin: EdgeInsets.only(bottom: 14 * context.sf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(16), color: bgColor, borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))], border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))],
), ),
child: Column( child: Column(
children: [ children: [
@@ -356,16 +406,16 @@ class _HomeScreenState extends State<HomeScreen> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)), Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), maxLines: 1, overflow: TextOverflow.ellipsis)),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf), padding: EdgeInsets.symmetric(horizontal: 8 * context.sf),
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), borderRadius: BorderRadius.circular(8)),
child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)), child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: textColor)),
), ),
), ),
Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)), Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)),
], ],
), ),
], ],
@@ -374,10 +424,10 @@ class _HomeScreenState extends State<HomeScreen> {
], ],
), ),
), ),
Divider(height: 1, color: Colors.grey.shade100, thickness: 1.5), Divider(height: 1, color: Colors.grey.withOpacity(0.1), thickness: 1.5),
Container( Container(
width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf), width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf),
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))), decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))),
child: Column( child: Column(
children: [ children: [
Row( Row(
@@ -413,13 +463,13 @@ class _HomeScreenState extends State<HomeScreen> {
children: [ children: [
Icon(icon, size: 14 * context.sf, color: color), Icon(icon, size: 14 * context.sf, color: color),
SizedBox(width: 4 * context.sf), SizedBox(width: 4 * context.sf),
Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)),
Expanded( Expanded(
child: Text( child: Text(
value, value,
style: TextStyle( style: TextStyle(
fontSize: 11 * context.sf, fontSize: 11 * context.sf,
color: isMvp ? Colors.amber.shade900 : Colors.black87, color: isMvp ? AppTheme.warningAmber : Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold fontWeight: FontWeight.bold
), ),
maxLines: 1, overflow: TextOverflow.ellipsis maxLines: 1, overflow: TextOverflow.ellipsis

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/controllers/login_controller.dart'; import 'package:playmaker/controllers/login_controller.dart';
import '../widgets/login_widgets.dart'; import '../widgets/login_widgets.dart';
import 'home.dart'; // <--- IMPORTANTE: Importa a tua HomeScreen import 'home.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER! import '../utils/size_extension.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
const LoginPage({super.key}); const LoginPage({super.key});
@@ -23,7 +23,8 @@ class _LoginPageState extends State<LoginPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, // 👇 Adaptável ao Modo Claro/Escuro do Flutter
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: SafeArea( body: SafeArea(
child: ListenableBuilder( child: ListenableBuilder(
listenable: controller, listenable: controller,
@@ -32,7 +33,6 @@ class _LoginPageState extends State<LoginPage> {
child: SingleChildScrollView( child: SingleChildScrollView(
child: Container( child: Container(
width: double.infinity, width: double.infinity,
// Garante que o form não fica gigante num tablet
constraints: BoxConstraints(maxWidth: 450 * context.sf), constraints: BoxConstraints(maxWidth: 450 * context.sf),
padding: EdgeInsets.all(32 * context.sf), padding: EdgeInsets.all(32 * context.sf),
child: Column( child: Column(

View File

@@ -0,0 +1,250 @@
import 'package:flutter/material.dart';
import 'package:playmaker/classe/theme.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../utils/size_extension.dart';
import 'login.dart';
// 👇 OBRIGATÓRIO IMPORTAR O MAIN.DART PARA LER A VARIÁVEL "themeNotifier"
import '../main.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
@override
Widget build(BuildContext context) {
// 👇 CORES DINÂMICAS (A MÁGICA DO MODO ESCURO)
final Color primaryRed = AppTheme.primaryRed;
final Color bgColor = Theme.of(context).scaffoldBackgroundColor;
final Color cardColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
final Color textColor = Theme.of(context).colorScheme.onSurface;
final Color textLightColor = textColor.withOpacity(0.6);
// 👇 SABER SE A APP ESTÁ ESCURA OU CLARA NESTE EXATO MOMENTO
bool isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: bgColor,
appBar: AppBar(
backgroundColor: primaryRed,
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
title: Text(
"Perfil e Definições",
style: TextStyle(
fontSize: 18 * context.sf,
fontWeight: FontWeight.w600,
),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
),
body: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 16.0 * context.sf, vertical: 24.0 * context.sf),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ==========================================
// CARTÃO DE PERFIL
// ==========================================
Container(
padding: EdgeInsets.all(20 * context.sf),
decoration: BoxDecoration(
color: cardColor,
borderRadius: BorderRadius.circular(16 * context.sf),
border: Border.all(color: Colors.grey.withOpacity(0.1)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
CircleAvatar(
radius: 32 * context.sf,
backgroundColor: primaryRed.withOpacity(0.1),
child: Icon(Icons.person, color: primaryRed, size: 32 * context.sf),
),
SizedBox(width: 16 * context.sf),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Treinador",
style: TextStyle(
fontSize: 18 * context.sf,
fontWeight: FontWeight.bold,
color: textColor,
),
),
SizedBox(height: 4 * context.sf),
Text(
Supabase.instance.client.auth.currentUser?.email ?? "sem@email.com",
style: TextStyle(
color: textLightColor,
fontSize: 14 * context.sf,
),
),
],
),
),
],
),
),
SizedBox(height: 32 * context.sf),
// ==========================================
// SECÇÃO: DEFINIÇÕES
// ==========================================
Padding(
padding: EdgeInsets.only(left: 4 * context.sf, bottom: 12 * context.sf),
child: Text(
"Definições",
style: TextStyle(
color: textLightColor,
fontSize: 14 * context.sf,
fontWeight: FontWeight.bold,
),
),
),
Container(
decoration: BoxDecoration(
color: cardColor,
borderRadius: BorderRadius.circular(16 * context.sf),
border: Border.all(color: Colors.grey.withOpacity(0.1)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 20 * context.sf, vertical: 8 * context.sf),
leading: Icon(isDark ? Icons.dark_mode : Icons.light_mode, color: primaryRed, size: 28 * context.sf),
title: Text(
"Modo Escuro",
style: TextStyle(fontWeight: FontWeight.bold, color: textColor, fontSize: 16 * context.sf),
),
subtitle: Text(
"Altera as cores da aplicação",
style: TextStyle(color: textLightColor, fontSize: 13 * context.sf),
),
trailing: Switch(
value: isDark,
activeColor: primaryRed,
onChanged: (bool value) {
// 👇 CHAMA A VARIÁVEL DO MAIN.DART E ATUALIZA A APP TODA
themeNotifier.value = value ? ThemeMode.dark : ThemeMode.light;
},
),
),
),
SizedBox(height: 32 * context.sf),
// ==========================================
// SECÇÃO: CONTA
// ==========================================
Padding(
padding: EdgeInsets.only(left: 4 * context.sf, bottom: 12 * context.sf),
child: Text(
"Conta",
style: TextStyle(
color: textLightColor,
fontSize: 14 * context.sf,
fontWeight: FontWeight.bold,
),
),
),
Container(
decoration: BoxDecoration(
color: cardColor,
borderRadius: BorderRadius.circular(16 * context.sf),
border: Border.all(color: Colors.grey.withOpacity(0.1)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 20 * context.sf, vertical: 4 * context.sf),
leading: Icon(Icons.logout_outlined, color: primaryRed, size: 26 * context.sf),
title: Text(
"Terminar Sessão",
style: TextStyle(
color: primaryRed,
fontWeight: FontWeight.bold,
fontSize: 15 * context.sf,
),
),
onTap: () => _confirmLogout(context), // 👇 CHAMA O LOGOUT REAL
),
),
SizedBox(height: 50 * context.sf),
// ==========================================
// VERSÃO DA APP
// ==========================================
Center(
child: Text(
"PlayMaker v1.0.0",
style: TextStyle(
color: textLightColor.withOpacity(0.7),
fontSize: 13 * context.sf,
),
),
),
SizedBox(height: 20 * context.sf),
],
),
),
);
}
// 👇 FUNÇÃO PARA FAZER LOGOUT
void _confirmLogout(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surface,
title: Text("Terminar Sessão", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
content: Text("Tens a certeza que queres sair da conta?", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancelar", style: TextStyle(color: Colors.grey))),
TextButton(
onPressed: () async {
await Supabase.instance.client.auth.signOut();
if (ctx.mounted) {
// Mata a navegação toda para trás e manda para o Login
Navigator.of(ctx).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const LoginPage()),
(Route<dynamic> route) => false,
);
}
},
child: Text("Sair", style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold))
),
],
),
);
}
}

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:playmaker/classe/theme.dart';
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../utils/size_extension.dart'; // 👇 A MAGIA DO SF! import '../utils/size_extension.dart';
class StatusPage extends StatefulWidget { class StatusPage extends StatefulWidget {
const StatusPage({super.key}); const StatusPage({super.key});
@@ -21,6 +22,9 @@ class _StatusPageState extends State<StatusPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bgColor = Theme.of(context).cardTheme.color ?? Colors.white;
final textColor = Theme.of(context).colorScheme.onSurface;
return Column( return Column(
children: [ children: [
Padding( Padding(
@@ -30,20 +34,20 @@ class _StatusPageState extends State<StatusPage> {
child: Container( child: Container(
padding: EdgeInsets.all(12 * context.sf), padding: EdgeInsets.all(12 * context.sf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: bgColor,
borderRadius: BorderRadius.circular(15 * context.sf), borderRadius: BorderRadius.circular(15 * context.sf),
border: Border.all(color: Colors.grey.shade300), border: Border.all(color: Colors.grey.withOpacity(0.2)),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)] boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)]
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row(children: [ Row(children: [
Icon(Icons.shield, color: const Color(0xFFE74C3C), size: 24 * context.sf), Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
SizedBox(width: 10 * context.sf), SizedBox(width: 10 * context.sf),
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold)) Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
]), ]),
const Icon(Icons.arrow_drop_down), Icon(Icons.arrow_drop_down, color: textColor),
], ],
), ),
), ),
@@ -63,7 +67,7 @@ class _StatusPageState extends State<StatusPage> {
stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!), stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
builder: (context, membersSnapshot) { builder: (context, membersSnapshot) {
if (statsSnapshot.connectionState == ConnectionState.waiting || gamesSnapshot.connectionState == ConnectionState.waiting || membersSnapshot.connectionState == ConnectionState.waiting) { if (statsSnapshot.connectionState == ConnectionState.waiting || gamesSnapshot.connectionState == ConnectionState.waiting || membersSnapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator(color: Color(0xFFE74C3C))); return Center(child: CircularProgressIndicator(color: AppTheme.primaryRed));
} }
final membersData = membersSnapshot.data ?? []; final membersData = membersSnapshot.data ?? [];
@@ -82,7 +86,7 @@ class _StatusPageState extends State<StatusPage> {
return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA); return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA);
}); });
return _buildStatsGrid(context, playerTotals, teamTotals); return _buildStatsGrid(context, playerTotals, teamTotals, bgColor, textColor);
} }
); );
} }
@@ -96,12 +100,10 @@ class _StatusPageState extends State<StatusPage> {
List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) { List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
Map<String, Map<String, dynamic>> aggregated = {}; Map<String, Map<String, dynamic>> aggregated = {};
for (var member in members) { for (var member in members) {
String name = member['name']?.toString() ?? "Desconhecido"; String name = member['name']?.toString() ?? "Desconhecido";
aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0}; aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
} }
for (var row in stats) { for (var row in stats) {
String name = row['player_name']?.toString() ?? "Desconhecido"; String name = row['player_name']?.toString() ?? "Desconhecido";
if (!aggregated.containsKey(name)) aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0}; if (!aggregated.containsKey(name)) aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
@@ -113,7 +115,6 @@ class _StatusPageState extends State<StatusPage> {
aggregated[name]!['stl'] += (row['stl'] ?? 0); aggregated[name]!['stl'] += (row['stl'] ?? 0);
aggregated[name]!['blk'] += (row['blk'] ?? 0); aggregated[name]!['blk'] += (row['blk'] ?? 0);
} }
for (var game in games) { for (var game in games) {
String? mvp = game['mvp_name']; String? mvp = game['mvp_name'];
String? defRaw = game['top_def_name']; String? defRaw = game['top_def_name'];
@@ -134,52 +135,53 @@ class _StatusPageState extends State<StatusPage> {
return {'name': 'TOTAL EQUIPA', 'j': teamGames, 'pts': tPts, 'ast': tAst, 'rbs': tRbs, 'stl': tStl, 'blk': tBlk, 'mvp': tMvp, 'def': tDef}; return {'name': 'TOTAL EQUIPA', 'j': teamGames, 'pts': tPts, 'ast': tAst, 'rbs': tRbs, 'stl': tStl, 'blk': tBlk, 'mvp': tMvp, 'def': tDef};
} }
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals) { Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, Color bgColor, Color textColor) {
return Container( return Container(
color: Colors.white, color: Colors.transparent,
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: DataTable( child: DataTable(
columnSpacing: 25 * context.sf, columnSpacing: 25 * context.sf,
headingRowColor: MaterialStateProperty.all(Colors.grey.shade100), headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
dataRowHeight: 60 * context.sf, dataRowMaxHeight: 60 * context.sf,
dataRowMinHeight: 60 * context.sf,
columns: [ columns: [
DataColumn(label: const Text('JOGADOR')), DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))),
_buildSortableColumn(context, 'J', 'j'), _buildSortableColumn(context, 'J', 'j', textColor),
_buildSortableColumn(context, 'PTS', 'pts'), _buildSortableColumn(context, 'PTS', 'pts', textColor),
_buildSortableColumn(context, 'AST', 'ast'), _buildSortableColumn(context, 'AST', 'ast', textColor),
_buildSortableColumn(context, 'RBS', 'rbs'), _buildSortableColumn(context, 'RBS', 'rbs', textColor),
_buildSortableColumn(context, 'STL', 'stl'), _buildSortableColumn(context, 'STL', 'stl', textColor),
_buildSortableColumn(context, 'BLK', 'blk'), _buildSortableColumn(context, 'BLK', 'blk', textColor),
_buildSortableColumn(context, 'DEF 🛡️', 'def'), _buildSortableColumn(context, 'DEF 🛡️', 'def', textColor),
_buildSortableColumn(context, 'MVP 🏆', 'mvp'), _buildSortableColumn(context, 'MVP 🏆', 'mvp', textColor),
], ],
rows: [ rows: [
...players.map((player) => DataRow(cells: [ ...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(Row(children: [CircleAvatar(radius: 15 * context.sf, backgroundColor: Colors.grey.withOpacity(0.2), child: Icon(Icons.person, size: 18 * context.sf, color: Colors.grey)), SizedBox(width: 10 * context.sf), Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor))])),
DataCell(Center(child: Text(player['j'].toString()))), DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))),
_buildStatCell(context, player['pts'], isHighlight: true), _buildStatCell(context, player['pts'], textColor, isHighlight: true),
_buildStatCell(context, player['ast']), _buildStatCell(context, player['ast'], textColor),
_buildStatCell(context, player['rbs']), _buildStatCell(context, player['rbs'], textColor),
_buildStatCell(context, player['stl']), _buildStatCell(context, player['stl'], textColor),
_buildStatCell(context, player['blk']), _buildStatCell(context, player['blk'], textColor),
_buildStatCell(context, player['def'], isBlue: true), _buildStatCell(context, player['def'], textColor, isBlue: true),
_buildStatCell(context, player['mvp'], isGold: true), _buildStatCell(context, player['mvp'], textColor, isGold: true),
])), ])),
DataRow( DataRow(
color: MaterialStateProperty.all(Colors.grey.shade50), color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)),
cells: [ cells: [
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.black, fontSize: 12 * context.sf))), DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: textColor, fontSize: 12 * context.sf))),
DataCell(Center(child: Text(teamTotals['j'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)))), DataCell(Center(child: Text(teamTotals['j'].toString(), style: TextStyle(fontWeight: FontWeight.bold, color: textColor)))),
_buildStatCell(context, teamTotals['pts'], isHighlight: true), _buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true),
_buildStatCell(context, teamTotals['ast']), _buildStatCell(context, teamTotals['ast'], textColor),
_buildStatCell(context, teamTotals['rbs']), _buildStatCell(context, teamTotals['rbs'], textColor),
_buildStatCell(context, teamTotals['stl']), _buildStatCell(context, teamTotals['stl'], textColor),
_buildStatCell(context, teamTotals['blk']), _buildStatCell(context, teamTotals['blk'], textColor),
_buildStatCell(context, teamTotals['def'], isBlue: true), _buildStatCell(context, teamTotals['def'], textColor, isBlue: true),
_buildStatCell(context, teamTotals['mvp'], isGold: true), _buildStatCell(context, teamTotals['mvp'], textColor, isGold: true),
] ]
) )
], ],
@@ -189,37 +191,37 @@ class _StatusPageState extends State<StatusPage> {
); );
} }
DataColumn _buildSortableColumn(BuildContext context, String title, String sortKey) { DataColumn _buildSortableColumn(BuildContext context, String title, String sortKey, Color textColor) {
return DataColumn(label: InkWell( return DataColumn(label: InkWell(
onTap: () => setState(() { onTap: () => setState(() {
if (_sortColumn == sortKey) _isAscending = !_isAscending; if (_sortColumn == sortKey) _isAscending = !_isAscending;
else { _sortColumn = sortKey; _isAscending = false; } else { _sortColumn = sortKey; _isAscending = false; }
}), }),
child: Row(children: [ child: Row(children: [
Text(title, style: TextStyle(fontSize: 12 * context.sf, fontWeight: FontWeight.bold)), Text(title, style: TextStyle(fontSize: 12 * context.sf, fontWeight: FontWeight.bold, color: textColor)),
if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * context.sf, color: const Color(0xFFE74C3C)), if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * context.sf, color: AppTheme.primaryRed),
]), ]),
)); ));
} }
DataCell _buildStatCell(BuildContext context, int value, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) { DataCell _buildStatCell(BuildContext context, int value, Color textColor, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) {
return DataCell(Center(child: Container( return DataCell(Center(child: Container(
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
decoration: BoxDecoration(color: isGold && value > 0 ? Colors.amber.withOpacity(0.2) : (isBlue && value > 0 ? Colors.blue.withOpacity(0.1) : Colors.transparent), borderRadius: BorderRadius.circular(6)), decoration: BoxDecoration(color: isGold && value > 0 ? Colors.amber.withOpacity(0.2) : (isBlue && value > 0 ? Colors.blue.withOpacity(0.1) : Colors.transparent), borderRadius: BorderRadius.circular(6)),
child: Text(value == 0 ? "-" : value.toString(), style: TextStyle( child: Text(value == 0 ? "-" : value.toString(), style: TextStyle(
fontWeight: (isHighlight || isGold || isBlue) ? FontWeight.w900 : FontWeight.w600, fontWeight: (isHighlight || isGold || isBlue) ? FontWeight.w900 : FontWeight.w600,
fontSize: 14 * context.sf, color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? Colors.green.shade700 : Colors.black87)) fontSize: 14 * context.sf, color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? AppTheme.successGreen : textColor))
)), )),
))); )));
} }
void _showTeamSelector(BuildContext context) { void _showTeamSelector(BuildContext context) {
showModalBottomSheet(context: context, builder: (context) => StreamBuilder<List<Map<String, dynamic>>>( showModalBottomSheet(context: context, backgroundColor: Theme.of(context).colorScheme.surface, builder: (context) => StreamBuilder<List<Map<String, dynamic>>>(
stream: _teamController.teamsStream, stream: _teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
final teams = snapshot.data ?? []; final teams = snapshot.data ?? [];
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile( return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile(
title: Text(teams[i]['name']), title: Text(teams[i]['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); }, onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); },
)); ));
}, },

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/screens/team_stats_page.dart'; import 'package:playmaker/screens/team_stats_page.dart';
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../models/team_model.dart'; import '../models/team_model.dart';
import '../utils/size_extension.dart'; // 👇 IMPORTANTE: O TEU NOVO SUPERPODER import '../utils/size_extension.dart';
class TeamsPage extends StatefulWidget { class TeamsPage extends StatefulWidget {
const TeamsPage({super.key}); const TeamsPage({super.key});
@@ -32,14 +33,14 @@ class _TeamsPageState extends State<TeamsPage> {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setModalState) { builder: (context, setModalState) {
return AlertDialog( return AlertDialog(
backgroundColor: const Color(0xFF2C3E50), backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text("Filtros de pesquisa", style: TextStyle(color: Colors.white, fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), Text("Filtros de pesquisa", style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
IconButton( IconButton(
icon: Icon(Icons.close, color: Colors.white, size: 20 * context.sf), icon: Icon(Icons.close, color: Colors.grey, size: 20 * context.sf),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
) )
], ],
@@ -47,7 +48,7 @@ class _TeamsPageState extends State<TeamsPage> {
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Divider(color: Colors.white24), Divider(color: Colors.grey.withOpacity(0.2)),
SizedBox(height: 16 * context.sf), SizedBox(height: 16 * context.sf),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -82,7 +83,7 @@ class _TeamsPageState extends State<TeamsPage> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text("CONCLUÍDO", style: TextStyle(color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), child: Text("CONCLUÍDO", style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * context.sf)),
), ),
], ],
); );
@@ -107,7 +108,7 @@ class _TeamsPageState extends State<TeamsPage> {
child: Text( child: Text(
opt, opt,
style: TextStyle( style: TextStyle(
color: isSelected ? const Color(0xFFE74C3C) : Colors.white70, color: isSelected ? AppTheme.primaryRed : Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 14 * context.sf, fontSize: 14 * context.sf,
), ),
@@ -121,16 +122,15 @@ class _TeamsPageState extends State<TeamsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 🔥 OLHA QUE LIMPEZA: Já não precisamos de calcular nada aqui!
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F7FA), backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
title: Text("Minhas Equipas", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)), title: Text("Minhas Equipas", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)),
backgroundColor: const Color(0xFFF5F7FA), backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 0, elevation: 0,
actions: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.filter_list, color: const Color(0xFFE74C3C), size: 24 * context.sf), icon: Icon(Icons.filter_list, color: AppTheme.primaryRed, size: 24 * context.sf),
onPressed: () => _showFilterDialog(context), onPressed: () => _showFilterDialog(context),
), ),
], ],
@@ -142,8 +142,8 @@ class _TeamsPageState extends State<TeamsPage> {
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'add_team_btn', // 👇 A MÁGICA ESTÁ AQUI! heroTag: 'add_team_btn',
backgroundColor: const Color(0xFFE74C3C), backgroundColor: AppTheme.primaryRed,
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf), child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
onPressed: () => _showCreateDialog(context), onPressed: () => _showCreateDialog(context),
), ),
@@ -156,13 +156,13 @@ class _TeamsPageState extends State<TeamsPage> {
child: TextField( child: TextField(
controller: _searchController, controller: _searchController,
onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()), onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()),
style: TextStyle(fontSize: 16 * context.sf), style: TextStyle(fontSize: 16 * context.sf, color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Pesquisar equipa...', hintText: 'Pesquisar equipa...',
hintStyle: TextStyle(fontSize: 16 * context.sf), hintStyle: TextStyle(fontSize: 16 * context.sf, color: Colors.grey),
prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * context.sf), prefixIcon: Icon(Icons.search, color: AppTheme.primaryRed, size: 22 * context.sf),
filled: true, filled: true,
fillColor: Colors.white, fillColor: Theme.of(context).colorScheme.surface, // 👇 Adapta-se ao Dark Mode
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * context.sf), borderSide: BorderSide.none), border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * context.sf), borderSide: BorderSide.none),
), ),
), ),
@@ -173,8 +173,8 @@ class _TeamsPageState extends State<TeamsPage> {
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: controller.teamsStream, stream: controller.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator()); if (snapshot.connectionState == ConnectionState.waiting) return Center(child: CircularProgressIndicator(color: AppTheme.primaryRed));
if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * context.sf))); if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * context.sf, color: Theme.of(context).colorScheme.onSurface)));
var data = List<Map<String, dynamic>>.from(snapshot.data!); var data = List<Map<String, dynamic>>.from(snapshot.data!);
@@ -201,6 +201,7 @@ class _TeamsPageState extends State<TeamsPage> {
team: team, team: team,
controller: controller, controller: controller,
onFavoriteTap: () => controller.toggleFavorite(team.id, team.isFavorite), onFavoriteTap: () => controller.toggleFavorite(team.id, team.isFavorite),
sf: context.sf,
), ),
); );
}, },
@@ -210,7 +211,7 @@ class _TeamsPageState extends State<TeamsPage> {
} }
void _showCreateDialog(BuildContext context) { void _showCreateDialog(BuildContext context) {
showDialog(context: context, builder: (context) => CreateTeamDialog(onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl))); showDialog(context: context, builder: (context) => CreateTeamDialog(sf: context.sf, onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl)));
} }
} }
@@ -219,73 +220,140 @@ class TeamCard extends StatelessWidget {
final Team team; final Team team;
final TeamController controller; final TeamController controller;
final VoidCallback onFavoriteTap; final VoidCallback onFavoriteTap;
final double sf;
const TeamCard({super.key, required this.team, required this.controller, required this.onFavoriteTap}); const TeamCard({
super.key,
required this.team,
required this.controller,
required this.onFavoriteTap,
required this.sf,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
color: Colors.white, elevation: 3, margin: EdgeInsets.only(bottom: 12 * context.sf), final textColor = Theme.of(context).colorScheme.onSurface;
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
child: ListTile( return Container(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 8 * context.sf), margin: EdgeInsets.only(bottom: 12 * sf),
leading: Stack( decoration: BoxDecoration(
clipBehavior: Clip.none, color: bgColor,
children: [ borderRadius: BorderRadius.circular(15 * sf),
CircleAvatar( border: Border.all(color: Colors.grey.withOpacity(0.15)),
radius: 28 * context.sf, backgroundColor: Colors.grey[200], boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10 * sf)]
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http')) ? NetworkImage(team.imageUrl) : null, ),
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http')) ? Text(team.imageUrl.isEmpty ? "🏀" : team.imageUrl, style: TextStyle(fontSize: 24 * context.sf)) : null, child: Material(
), color: Colors.transparent,
Positioned( borderRadius: BorderRadius.circular(15 * sf),
left: -15 * context.sf, top: -10 * context.sf, child: ListTile(
child: IconButton( contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf),
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)]), leading: Stack(
onPressed: onFavoriteTap, clipBehavior: Clip.none,
),
),
],
),
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: [ children: [
Icon(Icons.groups_outlined, size: 16 * context.sf, color: Colors.grey), CircleAvatar(
SizedBox(width: 4 * context.sf), radius: 28 * sf,
StreamBuilder<int>( backgroundColor: Colors.grey.withOpacity(0.2),
stream: controller.getPlayerCountStream(team.id), backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
initialData: 0, ? NetworkImage(team.imageUrl)
builder: (context, snapshot) { : null,
final count = snapshot.data ?? 0; child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
return Text("$count Jogs.", style: TextStyle(color: count > 0 ? Colors.green[700] : Colors.orange, fontWeight: FontWeight.bold, fontSize: 13 * context.sf)); ? Text(
}, team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
style: TextStyle(fontSize: 24 * sf),
)
: null,
),
Positioned(
left: -15 * sf,
top: -10 * sf,
child: IconButton(
icon: Icon(
team.isFavorite ? Icons.star : Icons.star_border,
color: team.isFavorite ? AppTheme.warningAmber : Theme.of(context).colorScheme.onSurface.withOpacity(0.2),
size: 28 * sf,
shadows: [
Shadow(color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), blurRadius: 4 * sf),
],
),
onPressed: onFavoriteTap,
),
),
],
),
title: Text(
team.name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf, color: textColor),
overflow: TextOverflow.ellipsis,
),
subtitle: Padding(
padding: EdgeInsets.only(top: 6.0 * sf),
child: Row(
children: [
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
SizedBox(width: 4 * 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 ? AppTheme.successGreen : AppTheme.warningAmber, // 👇 Usando cores do tema
fontWeight: FontWeight.bold,
fontSize: 13 * sf,
),
);
},
),
SizedBox(width: 8 * sf),
Expanded(
child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * 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 * sf),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))),
),
IconButton(
tooltip: 'Eliminar Equipa',
icon: Icon(Icons.delete_outline, color: AppTheme.primaryRed, size: 24 * sf),
onPressed: () => _confirmDelete(context, sf, bgColor, textColor),
), ),
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) { void _confirmDelete(BuildContext context, double sf, Color cardColor, Color textColor) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), backgroundColor: cardColor,
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * context.sf)), surfaceTintColor: Colors.transparent,
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold, color: textColor)),
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * sf, color: textColor)),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))), TextButton(
TextButton(onPressed: () { controller.deleteTeam(team.id); Navigator.pop(context); }, child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * context.sf))), onPressed: () => Navigator.pop(context),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf, color: Colors.grey)),
),
TextButton(
onPressed: () {
controller.deleteTeam(team.id);
Navigator.pop(context);
},
child: Text('Eliminar', style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf)),
),
], ],
), ),
); );
@@ -295,7 +363,9 @@ class TeamCard extends StatelessWidget {
// --- DIALOG DE CRIAÇÃO --- // --- DIALOG DE CRIAÇÃO ---
class CreateTeamDialog extends StatefulWidget { class CreateTeamDialog extends StatefulWidget {
final Function(String name, String season, String imageUrl) onConfirm; final Function(String name, String season, String imageUrl) onConfirm;
const CreateTeamDialog({super.key, required this.onConfirm}); final double sf;
const CreateTeamDialog({super.key, required this.onConfirm, required this.sf});
@override @override
State<CreateTeamDialog> createState() => _CreateTeamDialogState(); State<CreateTeamDialog> createState() => _CreateTeamDialogState();
@@ -309,31 +379,33 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)), backgroundColor: Theme.of(context).colorScheme.surface,
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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), TextField(controller: _nameController, style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * widget.sf)), textCapitalization: TextCapitalization.words),
SizedBox(height: 15 * context.sf), SizedBox(height: 15 * widget.sf),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf)), dropdownColor: Theme.of(context).colorScheme.surface,
style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87), value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * widget.sf)),
style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface),
items: ['2023/24', '2024/25', '2025/26'].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), items: ['2023/24', '2024/25', '2025/26'].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
onChanged: (val) => setState(() => _selectedSeason = val!), onChanged: (val) => setState(() => _selectedSeason = val!),
), ),
SizedBox(height: 15 * context.sf), SizedBox(height: 15 * widget.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))), TextField(controller: _imageController, style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration(labelText: 'URL Imagem ou Emoji', labelStyle: TextStyle(fontSize: 14 * widget.sf), hintText: 'Ex: 🏀 ou https://...', hintStyle: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))),
], ],
), ),
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))), TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)), style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)),
onPressed: () { if (_nameController.text.trim().isNotEmpty) { widget.onConfirm(_nameController.text.trim(), _selectedSeason, _imageController.text.trim()); Navigator.pop(context); } }, onPressed: () { if (_nameController.text.trim().isNotEmpty) { widget.onConfirm(_nameController.text.trim(), _selectedSeason, _imageController.text.trim()); Navigator.pop(context); } },
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * context.sf)), child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)),
), ),
], ],
); );

View File

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

View File

@@ -1,8 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA!
import '../models/team_model.dart'; import '../models/team_model.dart';
import '../models/person_model.dart'; import '../models/person_model.dart';
import '../utils/size_extension.dart'; // 👇 SUPERPODER SF
// --- CABEÇALHO --- // --- CABEÇALHO ---
class StatsHeader extends StatelessWidget { class StatsHeader extends StatelessWidget {
@@ -13,22 +15,24 @@ class StatsHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), padding: EdgeInsets.only(top: 50 * context.sf, left: 20 * context.sf, right: 20 * context.sf, bottom: 20 * context.sf),
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Color(0xFF2C3E50), color: AppTheme.primaryRed, // 👇 Usando a cor oficial
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(30), bottomRight: Radius.circular(30)), borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(30 * context.sf),
bottomRight: Radius.circular(30 * context.sf)
),
), ),
child: Row( child: Row(
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white), icon: Icon(Icons.arrow_back, color: Colors.white, size: 24 * context.sf),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
const SizedBox(width: 10), SizedBox(width: 10 * context.sf),
// IMAGEM OU EMOJI DA EQUIPA AQUI!
CircleAvatar( CircleAvatar(
radius: 24, radius: 24 * context.sf,
backgroundColor: Colors.white24, backgroundColor: Colors.white24,
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http')) backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
? NetworkImage(team.imageUrl) ? NetworkImage(team.imageUrl)
@@ -36,18 +40,25 @@ class StatsHeader extends StatelessWidget {
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http')) child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
? Text( ? Text(
team.imageUrl.isEmpty ? "🛡️" : team.imageUrl, team.imageUrl.isEmpty ? "🛡️" : team.imageUrl,
style: const TextStyle(fontSize: 20), style: TextStyle(fontSize: 20 * context.sf),
) )
: null, : null,
), ),
const SizedBox(width: 15), SizedBox(width: 15 * context.sf),
Expanded( // Expanded evita overflow se o nome for muito longo Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(team.name, style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis), Text(
Text(team.season, style: const TextStyle(color: Colors.white70, fontSize: 14)), team.name,
style: TextStyle(color: Colors.white, fontSize: 20 * context.sf, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis
),
Text(
team.season,
style: TextStyle(color: Colors.white70, fontSize: 14 * context.sf)
),
], ],
), ),
), ),
@@ -65,20 +76,36 @@ class StatsSummaryCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 👇 Adapta-se ao Modo Claro/Escuro
final Color bgColor = Theme.of(context).brightness == Brightness.dark ? const Color(0xFF1E1E1E) : Colors.white;
return Card( return Card(
elevation: 4, elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
child: Container( child: Container(
padding: const EdgeInsets.all(20), padding: EdgeInsets.all(20 * context.sf),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20), color: bgColor,
gradient: LinearGradient(colors: [Colors.blue.shade700, Colors.blue.shade400]), borderRadius: BorderRadius.circular(20 * context.sf),
border: Border.all(color: Colors.grey.withOpacity(0.15)),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text("Total de Membros", style: TextStyle(color: Colors.white, fontSize: 16)), Row(
Text("$total", style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)), children: [
Icon(Icons.groups, color: AppTheme.primaryRed, size: 28 * context.sf), // 👇 Cor do tema
SizedBox(width: 10 * context.sf),
Text(
"Total de Membros",
style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 16 * context.sf, fontWeight: FontWeight.w600)
),
],
),
Text(
"$total",
style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 28 * context.sf, fontWeight: FontWeight.bold)
),
], ],
), ),
), ),
@@ -97,8 +124,11 @@ class StatsSectionTitle extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF2C3E50))), Text(
const Divider(), title,
style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)
),
Divider(color: Colors.grey.withOpacity(0.2)),
], ],
); );
} }
@@ -121,37 +151,50 @@ class PersonCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 👇 Adapta as cores do Card ao Modo Escuro e ao Tema
final Color defaultBg = Theme.of(context).brightness == Brightness.dark ? const Color(0xFF1E1E1E) : Colors.white;
final Color coachBg = Theme.of(context).brightness == Brightness.dark ? AppTheme.warningAmber.withOpacity(0.1) : const Color(0xFFFFF9C4);
return Card( return Card(
margin: const EdgeInsets.only(top: 12), margin: EdgeInsets.only(top: 12 * context.sf),
elevation: 2, elevation: 2,
color: isCoach ? const Color(0xFFFFF9C4) : Colors.white, color: isCoach ? coachBg : defaultBg,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
child: ListTile( child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 4 * context.sf),
leading: isCoach leading: isCoach
? const CircleAvatar(backgroundColor: Colors.orange, child: Icon(Icons.person, color: Colors.white)) ? CircleAvatar(
radius: 22 * context.sf,
backgroundColor: AppTheme.warningAmber, // 👇 Cor do tema
child: Icon(Icons.person, color: Colors.white, size: 24 * context.sf)
)
: Container( : Container(
width: 45, width: 45 * context.sf,
height: 45, height: 45 * context.sf,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration(color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(10)), decoration: BoxDecoration(
child: Text(person.number ?? "J", style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 16)), color: AppTheme.primaryRed.withOpacity(0.1), // 👇 Cor do tema
borderRadius: BorderRadius.circular(10 * context.sf)
),
child: Text(
person.number ?? "J",
style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)
),
), ),
title: Text(person.name, style: const TextStyle(fontWeight: FontWeight.bold)), title: Text(
person.name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * context.sf, color: Theme.of(context).colorScheme.onSurface)
),
// --- CANTO DIREITO (Trailing) ---
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// IMAGEM DA EQUIPA NO CARD DO JOGADOR
const SizedBox(width: 5), // Espaço
IconButton( IconButton(
icon: const Icon(Icons.edit_outlined, color: Colors.blue), icon: Icon(Icons.edit_outlined, color: Colors.blue, size: 22 * context.sf),
onPressed: onEdit, onPressed: onEdit,
), ),
IconButton( IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red), icon: Icon(Icons.delete_outline, color: AppTheme.primaryRed, size: 22 * context.sf), // 👇 Cor do tema
onPressed: onDelete, onPressed: onDelete,
), ),
], ],
@@ -180,10 +223,9 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F7FA), backgroundColor: Theme.of(context).scaffoldBackgroundColor, // 👇 Adapta-se ao Modo Escuro
body: Column( body: Column(
children: [ children: [
// Cabeçalho
StatsHeader(team: widget.team), StatsHeader(team: widget.team),
Expanded( Expanded(
@@ -191,11 +233,11 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
stream: _controller.getMembers(widget.team.id), stream: _controller.getMembers(widget.team.id),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return Center(child: CircularProgressIndicator(color: AppTheme.primaryRed));
} }
if (snapshot.hasError) { if (snapshot.hasError) {
return Center(child: Text("Erro ao carregar: ${snapshot.error}")); return Center(child: Text("Erro ao carregar: ${snapshot.error}", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)));
} }
final members = snapshot.data ?? []; final members = snapshot.data ?? [];
@@ -204,15 +246,16 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
final players = members.where((m) => m.type == 'Jogador').toList(); final players = members.where((m) => m.type == 'Jogador').toList();
return RefreshIndicator( return RefreshIndicator(
color: AppTheme.primaryRed,
onRefresh: () async => setState(() {}), onRefresh: () async => setState(() {}),
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0 * context.sf),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
StatsSummaryCard(total: members.length), StatsSummaryCard(total: members.length),
const SizedBox(height: 30), SizedBox(height: 30 * context.sf),
// TREINADORES // TREINADORES
if (coaches.isNotEmpty) ...[ if (coaches.isNotEmpty) ...[
@@ -220,19 +263,18 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
...coaches.map((c) => PersonCard( ...coaches.map((c) => PersonCard(
person: c, person: c,
isCoach: true, isCoach: true,
onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, c), onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, c),
onDelete: () => _confirmDelete(context, c), onDelete: () => _confirmDelete(context, c),
)), )),
const SizedBox(height: 30), SizedBox(height: 30 * context.sf),
], ],
// JOGADORES // JOGADORES
const StatsSectionTitle(title: "Jogadores"), const StatsSectionTitle(title: "Jogadores"),
if (players.isEmpty) if (players.isEmpty)
const Padding( Padding(
padding: EdgeInsets.only(top: 20), padding: EdgeInsets.only(top: 20 * context.sf),
child: Text("Nenhum jogador nesta equipa.", style: TextStyle(color: Colors.grey, fontSize: 16)), child: Text("Nenhum jogador nesta equipa.", style: TextStyle(color: Colors.grey, fontSize: 16 * context.sf)),
) )
else else
...players.map((p) => PersonCard( ...players.map((p) => PersonCard(
@@ -241,7 +283,7 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, p), onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, p),
onDelete: () => _confirmDelete(context, p), onDelete: () => _confirmDelete(context, p),
)), )),
const SizedBox(height: 80), SizedBox(height: 80 * context.sf),
], ],
), ),
), ),
@@ -254,8 +296,8 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'fab_team_${widget.team.id}', heroTag: 'fab_team_${widget.team.id}',
onPressed: () => _controller.showAddPersonDialog(context, widget.team.id), onPressed: () => _controller.showAddPersonDialog(context, widget.team.id),
backgroundColor: const Color(0xFF00C853), backgroundColor: AppTheme.successGreen, // 👇 Cor de sucesso do tema
child: const Icon(Icons.add, color: Colors.white), child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
), ),
); );
} }
@@ -264,16 +306,20 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text("Eliminar Membro?"), backgroundColor: Theme.of(context).colorScheme.surface,
content: Text("Tens a certeza que queres remover ${person.name}?"), title: Text("Eliminar Membro?", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
content: Text("Tens a certeza que queres remover ${person.name}?", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancelar")), TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text("Cancelar", style: TextStyle(color: Colors.grey))
),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await _controller.deletePerson(person.id); await _controller.deletePerson(person.id);
if (ctx.mounted) Navigator.pop(ctx); if (ctx.mounted) Navigator.pop(ctx);
}, },
child: const Text("Eliminar", style: TextStyle(color: Colors.red)), child: Text("Eliminar", style: TextStyle(color: AppTheme.primaryRed)), // 👇 Cor oficial
), ),
], ],
), ),
@@ -323,20 +369,27 @@ class StatsController {
context: context, context: context,
builder: (ctx) => StatefulBuilder( builder: (ctx) => StatefulBuilder(
builder: (ctx, setState) => AlertDialog( builder: (ctx, setState) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), backgroundColor: Theme.of(context).colorScheme.surface,
title: Text(isEdit ? "Editar Membro" : "Novo Membro"), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
title: Text(
isEdit ? "Editar Membro" : "Novo Membro",
style: TextStyle(color: Theme.of(context).colorScheme.onSurface)
),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField( TextField(
controller: nameCtrl, controller: nameCtrl,
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
decoration: const InputDecoration(labelText: "Nome Completo"), decoration: const InputDecoration(labelText: "Nome Completo"),
textCapitalization: TextCapitalization.words, textCapitalization: TextCapitalization.words,
), ),
const SizedBox(height: 15), SizedBox(height: 15 * context.sf),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: selectedType, value: selectedType,
dropdownColor: Theme.of(context).colorScheme.surface,
style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 16 * context.sf),
decoration: const InputDecoration(labelText: "Função"), decoration: const InputDecoration(labelText: "Função"),
items: ["Jogador", "Treinador"] items: ["Jogador", "Treinador"]
.map((e) => DropdownMenuItem(value: e, child: Text(e))) .map((e) => DropdownMenuItem(value: e, child: Text(e)))
@@ -346,9 +399,10 @@ class StatsController {
}, },
), ),
if (selectedType == "Jogador") ...[ if (selectedType == "Jogador") ...[
const SizedBox(height: 15), SizedBox(height: 15 * context.sf),
TextField( TextField(
controller: numCtrl, controller: numCtrl,
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
decoration: const InputDecoration(labelText: "Número da Camisola"), decoration: const InputDecoration(labelText: "Número da Camisola"),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
@@ -359,12 +413,13 @@ class StatsController {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), onPressed: () => Navigator.pop(ctx),
child: const Text("Cancelar") child: const Text("Cancelar", style: TextStyle(color: Colors.grey))
), ),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00C853), backgroundColor: AppTheme.successGreen, // 👇 Cor verde do tema
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)) foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8 * context.sf))
), ),
onPressed: () async { onPressed: () async {
if (nameCtrl.text.trim().isEmpty) return; if (nameCtrl.text.trim().isEmpty) return;
@@ -397,12 +452,12 @@ class StatsController {
errorMsg = "Já existe um membro com este numero na equipa."; errorMsg = "Já existe um membro com este numero na equipa.";
} }
ScaffoldMessenger.of(ctx).showSnackBar( ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(content: Text(errorMsg), backgroundColor: Colors.red) SnackBar(content: Text(errorMsg), backgroundColor: AppTheme.primaryRed) // 👇 Cor oficial para erro
); );
} }
} }
}, },
child: const Text("Guardar", style: TextStyle(color: Colors.white)), child: const Text("Guardar"),
) )
], ],
), ),

View File

@@ -1,15 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:math' as math; import 'dart:math' as math;
// Esta extensão adiciona o superpoder "sf" ao BuildContext
extension SizeExtension on BuildContext { extension SizeExtension on BuildContext {
double get sf { double get sf {
final double wScreen = MediaQuery.of(this).size.width; final double wScreen = MediaQuery.of(this).size.width;
final double hScreen = MediaQuery.of(this).size.height; final double hScreen = MediaQuery.of(this).size.height;
// Ajusta a escala baseada no ecrã (muda os valores 1150/720 conforme a tua calibração)
// Calcula e devolve a escala na hora! return math.min(wScreen / 1150, hScreen / 720);
return math.min(wScreen, hScreen) / 400;
} }
} }

View File

@@ -1,104 +1,83 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/pages/PlacarPage.dart'; import 'package:playmaker/pages/PlacarPage.dart';
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA!
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../controllers/game_controller.dart'; import '../controllers/game_controller.dart';
// --- CARD DE EXIBIÇÃO DO JOGO ---
class GameResultCard extends StatelessWidget { class GameResultCard extends StatelessWidget {
final String gameId; final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
final String myTeam, opponentTeam, myScore, opponentScore, status, season; final String? myTeamLogo, opponentTeamLogo;
final String? myTeamLogo; final double sf;
final String? opponentTeamLogo;
final double sf; // NOVA VARIÁVEL DE ESCALA
const GameResultCard({ const GameResultCard({
super.key, super.key, required this.gameId, required this.myTeam, required this.opponentTeam,
required this.gameId, required this.myScore, required this.opponentScore, required this.status, required this.season,
required this.myTeam, this.myTeamLogo, this.opponentTeamLogo, required this.sf,
required this.opponentTeam,
required this.myScore,
required this.opponentScore,
required this.status,
required this.season,
this.myTeamLogo,
this.opponentTeamLogo,
required this.sf, // OBRIGATÓRIO RECEBER A ESCALA
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 👇 Puxa as cores de fundo dependendo do Modo (Claro/Escuro)
final bgColor = Theme.of(context).colorScheme.surface;
final textColor = Theme.of(context).colorScheme.onSurface;
return Container( return Container(
margin: EdgeInsets.only(bottom: 16 * sf), margin: EdgeInsets.only(bottom: 16 * sf),
padding: EdgeInsets.all(16 * sf), padding: EdgeInsets.all(16 * sf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: bgColor, // Usa a cor do tema
borderRadius: BorderRadius.circular(20 * sf), borderRadius: BorderRadius.circular(20 * sf),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)], boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)],
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, sf)), Expanded(child: _buildTeamInfo(myTeam, AppTheme.primaryRed, myTeamLogo, sf, textColor)), // Usa o primaryRed
_buildScoreCenter(context, gameId, sf), _buildScoreCenter(context, gameId, sf),
Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, sf)), Expanded(child: _buildTeamInfo(opponentTeam, textColor, opponentTeamLogo, sf, textColor)),
], ],
), ),
); );
} }
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf) { Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) {
return Column( return Column(
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 24 * sf, // Ajuste do tamanho do logo radius: 24 * sf,
backgroundColor: color, backgroundColor: color,
backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null,
? NetworkImage(logoUrl) child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * sf) : null,
: null,
child: (logoUrl == null || logoUrl.isEmpty)
? Icon(Icons.shield, color: Colors.white, size: 24 * sf)
: null,
), ),
SizedBox(height: 6 * sf), SizedBox(height: 6 * sf),
Text(name, Text(name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf, color: textColor), // Adapta à noite/dia
textAlign: TextAlign.center, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2,
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, double sf) {
final textColor = Theme.of(context).colorScheme.onSurface;
return Column( return Column(
children: [ children: [
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_scoreBox(myScore, Colors.green, sf), _scoreBox(myScore, AppTheme.successGreen, sf), // Verde do tema
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf)), Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf, color: textColor)),
_scoreBox(opponentScore, Colors.grey, sf), _scoreBox(opponentScore, Colors.grey, sf),
], ],
), ),
SizedBox(height: 10 * sf), SizedBox(height: 10 * sf),
TextButton.icon( TextButton.icon(
onPressed: () { onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))),
Navigator.push( icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: AppTheme.primaryRed),
context, label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: AppTheme.primaryRed, fontWeight: FontWeight.bold)),
MaterialPageRoute(
builder: (context) => PlacarPage(
gameId: id,
myTeam: myTeam,
opponentTeam: opponentTeam,
),
),
);
},
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)),
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), backgroundColor: AppTheme.primaryRed.withOpacity(0.1),
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf), padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)),
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
@@ -115,204 +94,4 @@ class GameResultCard extends StatelessWidget {
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)), decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)),
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)), child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
); );
}
// --- POPUP DE CRIAÇÃO ---
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,
});
@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 * widget.sf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * widget.sf)),
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)
),
),
SizedBox(height: 15 * widget.sf),
_buildSearch(label: "Minha Equipa", controller: _myTeamController, sf: widget.sf),
Padding(
padding: EdgeInsets.symmetric(vertical: 10 * widget.sf),
child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * widget.sf))
),
_buildSearch(label: "Adversário", controller: _opponentController, sf: widget.sf),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf))
),
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)
),
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 * 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)),
),
],
);
}
Widget _buildSearch({required String label, required TextEditingController controller, required double sf}) {
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 * sf),
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 250 * 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 * 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)
: null,
),
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * 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 * sf),
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(fontSize: 14 * sf),
prefixIcon: Icon(Icons.search, size: 20 * sf),
border: const OutlineInputBorder()
),
);
},
);
},
);
}
} }

View File

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

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/controllers/login_controller.dart'; import 'package:playmaker/controllers/login_controller.dart';
import 'package:playmaker/pages/RegisterPage.dart'; import 'package:playmaker/pages/RegisterPage.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER! import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA
import '../utils/size_extension.dart';
class BasketTrackHeader extends StatelessWidget { class BasketTrackHeader extends StatelessWidget {
const BasketTrackHeader({super.key}); const BasketTrackHeader({super.key});
@@ -11,7 +12,7 @@ class BasketTrackHeader extends StatelessWidget {
return Column( return Column(
children: [ children: [
SizedBox( SizedBox(
width: 200 * context.sf, // Ajusta o tamanho da imagem suavemente width: 200 * context.sf,
height: 200 * context.sf, height: 200 * context.sf,
child: Image.asset( child: Image.asset(
'assets/playmaker-logos.png', 'assets/playmaker-logos.png',
@@ -23,7 +24,7 @@ class BasketTrackHeader extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 36 * context.sf, fontSize: 36 * context.sf,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.grey[900], color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável ao Modo Escuro
), ),
), ),
SizedBox(height: 6 * context.sf), SizedBox(height: 6 * context.sf),
@@ -31,7 +32,7 @@ class BasketTrackHeader extends StatelessWidget {
'Gere as tuas equipas e estatísticas', 'Gere as tuas equipas e estatísticas',
style: TextStyle( style: TextStyle(
fontSize: 16 * context.sf, fontSize: 16 * context.sf,
color: Colors.grey[600], color: Colors.grey, // Mantemos cinza para subtítulo
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -52,13 +53,17 @@ class LoginFormFields extends StatelessWidget {
children: [ children: [
TextField( TextField(
controller: controller.emailController, controller: controller.emailController,
style: TextStyle(fontSize: 15 * context.sf), style: TextStyle(fontSize: 15 * context.sf, color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'E-mail', labelText: 'E-mail',
labelStyle: TextStyle(fontSize: 15 * context.sf), labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf), prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf, color: AppTheme.primaryRed), // 👇 Cor do tema
errorText: controller.emailError, errorText: controller.emailError,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12 * context.sf),
borderSide: BorderSide(color: AppTheme.primaryRed, width: 2), // 👇 Cor do tema ao focar
),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
), ),
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
@@ -67,16 +72,21 @@ class LoginFormFields extends StatelessWidget {
TextField( TextField(
controller: controller.passwordController, controller: controller.passwordController,
obscureText: controller.obscurePassword, obscureText: controller.obscurePassword,
style: TextStyle(fontSize: 15 * context.sf), style: TextStyle(fontSize: 15 * context.sf, color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Palavra-passe', labelText: 'Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * context.sf), labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf), prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf, color: AppTheme.primaryRed), // 👇 Cor do tema
errorText: controller.passwordError, errorText: controller.passwordError,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12 * context.sf),
borderSide: BorderSide(color: AppTheme.primaryRed, width: 2), // 👇 Cor do tema ao focar
),
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon( icon: Icon(
controller.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, controller.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
size: 22 * context.sf size: 22 * context.sf,
color: Colors.grey,
), ),
onPressed: controller.togglePasswordVisibility, onPressed: controller.togglePasswordVisibility,
), ),
@@ -106,7 +116,7 @@ class LoginButton extends StatelessWidget {
if (success) onLoginSuccess(); if (success) onLoginSuccess();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C), backgroundColor: AppTheme.primaryRed, // 👇 Usando a cor do tema
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
elevation: 3, elevation: 3,
@@ -135,8 +145,8 @@ class CreateAccountButton extends StatelessWidget {
Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterPage())); Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterPage()));
}, },
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFE74C3C), foregroundColor: AppTheme.primaryRed, // 👇 Usando a cor do tema
side: BorderSide(color: const Color(0xFFE74C3C), width: 2 * context.sf), side: BorderSide(color: AppTheme.primaryRed, width: 2 * context.sf), // 👇 Usando a cor do tema
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
), ),
child: Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), child: Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),

View File

@@ -1,7 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:playmaker/controllers/placar_controller.dart'; import 'package:playmaker/controllers/placar_controller.dart';
import 'package:playmaker/utils/size_extension.dart';
import 'package:playmaker/classe/theme.dart';
import 'dart:math' as math;
// --- PLACAR SUPERIOR --- import 'package:playmaker/zone_map_dialog.dart';
// ============================================================================
// 1. PLACAR SUPERIOR (CRONÓMETRO E RESULTADO)
// ============================================================================
class TopScoreboard extends StatelessWidget { class TopScoreboard extends StatelessWidget {
final PlacarController controller; final PlacarController controller;
final double sf; final double sf;
@@ -13,7 +21,7 @@ class TopScoreboard extends StatelessWidget {
return Container( return Container(
padding: EdgeInsets.symmetric(vertical: 10 * sf, horizontal: 35 * sf), padding: EdgeInsets.symmetric(vertical: 10 * sf, horizontal: 35 * sf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF16202C), color: AppTheme.placarDarkSurface,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(22 * sf), bottomLeft: Radius.circular(22 * sf),
bottomRight: Radius.circular(22 * sf) bottomRight: Radius.circular(22 * sf)
@@ -23,7 +31,7 @@ class TopScoreboard extends StatelessWidget {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false, sf), _buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, AppTheme.myTeamBlue, false, sf),
SizedBox(width: 30 * sf), SizedBox(width: 30 * sf),
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -31,7 +39,7 @@ class TopScoreboard extends StatelessWidget {
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 18 * sf, vertical: 5 * sf), padding: EdgeInsets.symmetric(horizontal: 18 * sf, vertical: 5 * sf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF2C3E50), color: AppTheme.placarTimerBg,
borderRadius: BorderRadius.circular(9 * sf) borderRadius: BorderRadius.circular(9 * sf)
), ),
child: Text( child: Text(
@@ -42,12 +50,12 @@ class TopScoreboard extends StatelessWidget {
SizedBox(height: 5 * sf), SizedBox(height: 5 * sf),
Text( Text(
"PERÍODO ${controller.currentQuarter}", "PERÍODO ${controller.currentQuarter}",
style: TextStyle(color: Colors.orangeAccent, fontSize: 14 * sf, fontWeight: FontWeight.w900) style: TextStyle(color: AppTheme.warningAmber, fontSize: 14 * sf, fontWeight: FontWeight.w900)
), ),
], ],
), ),
SizedBox(width: 30 * sf), SizedBox(width: 30 * sf),
_buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true, sf), _buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, AppTheme.oppTeamRed, true, sf),
], ],
), ),
); );
@@ -63,7 +71,7 @@ class TopScoreboard extends StatelessWidget {
width: 12 * sf, height: 12 * sf, width: 12 * sf, height: 12 * sf,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: index < timeouts ? Colors.yellow : Colors.grey.shade600, color: index < timeouts ? AppTheme.warningAmber : Colors.grey.shade600,
border: Border.all(color: Colors.white54, width: 1.5 * sf) border: Border.all(color: Colors.white54, width: 1.5 * sf)
), ),
)), )),
@@ -88,7 +96,7 @@ class TopScoreboard extends StatelessWidget {
SizedBox(height: 5 * sf), SizedBox(height: 5 * sf),
Text( Text(
"FALTAS: $displayFouls", "FALTAS: $displayFouls",
style: TextStyle(color: displayFouls >= 5 ? Colors.redAccent : Colors.yellowAccent, fontSize: 13 * sf, fontWeight: FontWeight.bold) style: TextStyle(color: displayFouls >= 5 ? AppTheme.actionMiss : AppTheme.warningAmber, fontSize: 13 * sf, fontWeight: FontWeight.bold)
), ),
], ],
) )
@@ -105,7 +113,9 @@ class TopScoreboard extends StatelessWidget {
); );
} }
// --- BANCO DE SUPLENTES --- // ============================================================================
// 2. BANCO DE SUPLENTES (DRAG & DROP)
// ============================================================================
class BenchPlayersList extends StatelessWidget { class BenchPlayersList extends StatelessWidget {
final PlacarController controller; final PlacarController controller;
final bool isOpponent; final bool isOpponent;
@@ -116,7 +126,7 @@ class BenchPlayersList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bench = isOpponent ? controller.oppBench : controller.myBench; final bench = isOpponent ? controller.oppBench : controller.myBench;
final teamColor = isOpponent ? const Color(0xFFD92C2C) : const Color(0xFF1E5BB2); final teamColor = isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue;
final prefix = isOpponent ? "bench_opp_" : "bench_my_"; final prefix = isOpponent ? "bench_opp_" : "bench_my_";
return Column( return Column(
@@ -150,7 +160,7 @@ class BenchPlayersList extends StatelessWidget {
if (isFouledOut) { if (isFouledOut) {
return GestureDetector( return GestureDetector(
onTap: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $playerName não pode voltar (Expulso).'), backgroundColor: Colors.red)), onTap: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $playerName não pode voltar (Expulso).'), backgroundColor: AppTheme.actionMiss)),
child: avatarUI child: avatarUI
); );
} }
@@ -173,18 +183,20 @@ class BenchPlayersList extends StatelessWidget {
} }
} }
// --- CARTÃO DO JOGADOR NO CAMPO --- // ============================================================================
// 3. CARTÃO DO JOGADOR NO CAMPO (TARGET DE FALTAS/PONTOS/SUBSTITUIÇÕES)
// ============================================================================
class PlayerCourtCard extends StatelessWidget { class PlayerCourtCard extends StatelessWidget {
final PlacarController controller; final PlacarController controller;
final String name; final String name;
final bool isOpponent; final bool isOpponent;
final double sf; 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, required this.sf});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final teamColor = isOpponent ? const Color(0xFFD92C2C) : const Color(0xFF1E5BB2); final teamColor = isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue;
final stats = controller.playerStats[name]!; final stats = controller.playerStats[name]!;
final number = controller.playerNumbers[name]!; final number = controller.playerNumbers[name]!;
final prefix = isOpponent ? "player_opp_" : "player_my_"; final prefix = isOpponent ? "player_opp_" : "player_my_";
@@ -194,18 +206,36 @@ class PlayerCourtCard extends StatelessWidget {
feedback: Material( feedback: Material(
color: Colors.transparent, color: Colors.transparent,
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 18 * sf, vertical: 11 * sf), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(9 * sf)), decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(8)),
child: Text(name, style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)), 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, sf)),
child: DragTarget<String>( child: DragTarget<String>(
onAcceptWithDetails: (details) { onAcceptWithDetails: (details) {
final action = details.data; final action = details.data;
if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) {
if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") {
bool isMake = action.startsWith("add_");
bool is3Pt = action.endsWith("_3");
showDialog(
context: context,
builder: (ctx) => ZoneMapDialog(
playerName: name,
isMake: isMake,
is3PointAction: is3Pt,
onZoneSelected: (zone, points, relX, relY) {
controller.registerShotFromPopup(context, action, "$prefix$name", zone, points, relX, relY);
},
),
);
}
else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) {
controller.handleActionDrag(context, action, "$prefix$name"); controller.handleActionDrag(context, action, "$prefix$name");
} else if (action.startsWith("bench_")) { }
else if (action.startsWith("bench_")) {
controller.handleSubbing(context, action, name, isOpponent); controller.handleSubbing(context, action, name, isOpponent);
} }
}, },
@@ -220,13 +250,13 @@ class PlayerCourtCard extends StatelessWidget {
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, double sf) {
bool isFouledOut = stats["fls"]! >= 5; 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; Color borderColor = isFouledOut ? AppTheme.actionMiss : Colors.transparent;
if (isSubbing) { if (isSubbing) {
bgColor = Colors.blue.shade50; borderColor = Colors.blue; bgColor = Colors.blue.shade50; borderColor = AppTheme.myTeamBlue;
} else if (isActionHover && !isFouledOut) { } else if (isActionHover && !isFouledOut) {
bgColor = Colors.orange.shade50; borderColor = Colors.orange; bgColor = Colors.orange.shade50; borderColor = AppTheme.actionPoints;
} }
int fgm = stats["fgm"]!; int fgm = stats["fgm"]!;
@@ -235,11 +265,10 @@ class PlayerCourtCard extends StatelessWidget {
String displayName = name.length > 12 ? "${name.substring(0, 10)}..." : name; String displayName = name.length > 12 ? "${name.substring(0, 10)}..." : name;
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: bgColor, color: bgColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: borderColor, width: 2),
borderRadius: BorderRadius.circular(11 * sf), boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 3))],
border: Border.all(color: borderColor, width: 1.8 * sf),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5 * sf, offset: Offset(2 * sf, 3.5 * sf))],
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(9 * sf), borderRadius: BorderRadius.circular(9 * sf),
@@ -262,16 +291,16 @@ class PlayerCourtCard extends StatelessWidget {
children: [ children: [
Text( Text(
displayName, displayName,
style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? Colors.red : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none) style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? AppTheme.actionMiss : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)
), ),
SizedBox(height: 2.5 * sf), SizedBox(height: 2.5 * sf),
Text( Text(
"${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", "${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)",
style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[700], fontWeight: FontWeight.w600) style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[700], fontWeight: FontWeight.w600)
), ),
Text( Text(
"${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls", "${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) style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[500], fontWeight: FontWeight.w600)
), ),
], ],
), ),
@@ -284,7 +313,9 @@ class PlayerCourtCard extends StatelessWidget {
} }
} }
// --- PAINEL DE BOTÕES DE AÇÃO --- // ============================================================================
// 4. PAINEL DE BOTÕES DE AÇÃO (PONTOS, RESSALTOS, ETC)
// ============================================================================
class ActionButtonsPanel extends StatelessWidget { class ActionButtonsPanel extends StatelessWidget {
final PlacarController controller; final PlacarController controller;
final double sf; final double sf;
@@ -293,8 +324,8 @@ class ActionButtonsPanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double baseSize = 65 * sf; // Reduzido (Antes era 75) final double baseSize = 65 * sf;
final double feedSize = 82 * sf; // Reduzido (Antes era 95) final double feedSize = 82 * sf;
final double gap = 7 * sf; final double gap = 7 * sf;
return Row( return Row(
@@ -302,30 +333,30 @@ class ActionButtonsPanel extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
_columnBtn([ _columnBtn([
_dragAndTargetBtn("M1", Colors.redAccent, "miss_1", baseSize, feedSize, sf), _dragAndTargetBtn("M1", AppTheme.actionMiss, "miss_1", baseSize, feedSize, sf),
_dragAndTargetBtn("1", Colors.orange, "add_pts_1", baseSize, feedSize, sf), _dragAndTargetBtn("1", AppTheme.actionPoints, "add_pts_1", baseSize, feedSize, sf),
_dragAndTargetBtn("1", Colors.orange, "sub_pts_1", baseSize, feedSize, sf, isX: true), _dragAndTargetBtn("1", AppTheme.actionPoints, "sub_pts_1", baseSize, feedSize, sf, isX: true),
_dragAndTargetBtn("STL", Colors.green, "add_stl", baseSize, feedSize, sf), _dragAndTargetBtn("STL", AppTheme.actionSteal, "add_stl", baseSize, feedSize, sf),
], gap), ], gap),
SizedBox(width: gap * 1), SizedBox(width: gap * 1),
_columnBtn([ _columnBtn([
_dragAndTargetBtn("M2", Colors.redAccent, "miss_2", baseSize, feedSize, sf), _dragAndTargetBtn("M2", AppTheme.actionMiss, "miss_2", baseSize, feedSize, sf),
_dragAndTargetBtn("2", Colors.orange, "add_pts_2", baseSize, feedSize, sf), _dragAndTargetBtn("2", AppTheme.actionPoints, "add_pts_2", baseSize, feedSize, sf),
_dragAndTargetBtn("2", Colors.orange, "sub_pts_2", baseSize, feedSize, sf, isX: true), _dragAndTargetBtn("2", AppTheme.actionPoints, "sub_pts_2", baseSize, feedSize, sf, isX: true),
_dragAndTargetBtn("AST", Colors.blueGrey, "add_ast", baseSize, feedSize, sf), _dragAndTargetBtn("AST", AppTheme.actionAssist, "add_ast", baseSize, feedSize, sf),
], gap), ], gap),
SizedBox(width: gap * 1), SizedBox(width: gap * 1),
_columnBtn([ _columnBtn([
_dragAndTargetBtn("M3", Colors.redAccent, "miss_3", baseSize, feedSize, sf), _dragAndTargetBtn("M3", AppTheme.actionMiss, "miss_3", baseSize, feedSize, sf),
_dragAndTargetBtn("3", Colors.orange, "add_pts_3", baseSize, feedSize, sf), _dragAndTargetBtn("3", AppTheme.actionPoints, "add_pts_3", baseSize, feedSize, sf),
_dragAndTargetBtn("3", Colors.orange, "sub_pts_3", baseSize, feedSize, sf, isX: true), _dragAndTargetBtn("3", AppTheme.actionPoints, "sub_pts_3", baseSize, feedSize, sf, isX: true),
_dragAndTargetBtn("TOV", Colors.redAccent, "add_tov", baseSize, feedSize, sf), _dragAndTargetBtn("TOV", AppTheme.actionMiss, "add_tov", baseSize, feedSize, sf),
], gap), ], gap),
SizedBox(width: gap * 1), SizedBox(width: gap * 1),
_columnBtn([ _columnBtn([
_dragAndTargetBtn("ORB", const Color(0xFF1E2A38), "add_orb", baseSize, feedSize, sf, icon: Icons.sports_basketball), _dragAndTargetBtn("ORB", AppTheme.actionRebound, "add_orb", baseSize, feedSize, sf, icon: Icons.sports_basketball),
_dragAndTargetBtn("DRB", const Color(0xFF1E2A38), "add_drb", baseSize, feedSize, sf, icon: Icons.sports_basketball), _dragAndTargetBtn("DRB", AppTheme.actionRebound, "add_drb", baseSize, feedSize, sf, icon: Icons.sports_basketball),
_dragAndTargetBtn("BLK", Colors.deepPurple, "add_blk", baseSize, feedSize, sf, icon: Icons.front_hand), _dragAndTargetBtn("BLK", AppTheme.actionBlock, "add_blk", baseSize, feedSize, sf, icon: Icons.front_hand),
], gap), ], gap),
], ],
); );
@@ -347,7 +378,7 @@ class ActionButtonsPanel extends StatelessWidget {
child: _circle(label, color, icon, false, baseSize, feedSize, sf, isX: isX) child: _circle(label, color, icon, false, baseSize, feedSize, sf, isX: isX)
), ),
child: DragTarget<String>( child: DragTarget<String>(
onAcceptWithDetails: (details) {}, onAcceptWithDetails: (details) {},
builder: (context, candidateData, rejectedData) { builder: (context, candidateData, rejectedData) {
bool isHovered = candidateData.any((data) => data != null && data.startsWith("player_")); bool isHovered = candidateData.any((data) => data != null && data.startsWith("player_"));
return Transform.scale( return Transform.scale(
@@ -408,7 +439,9 @@ class ActionButtonsPanel extends StatelessWidget {
children: [ children: [
Container( Container(
width: size, height: size, 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))]), 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, alignment: Alignment.center,
child: content, child: content,
), ),
@@ -416,4 +449,648 @@ class ActionButtonsPanel extends StatelessWidget {
], ],
); );
} }
}
// ============================================================================
// 5. PÁGINA DO PLACAR
// ============================================================================
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();
}
class _PlacarPageState extends State<PlacarPage> {
late PlacarController _controller;
@override
void initState() {
super.initState();
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeLeft,
]);
_controller = PlacarController(
gameId: widget.gameId,
myTeam: widget.myTeam,
opponentTeam: widget.opponentTeam,
onUpdate: () {
if (mounted) setState(() {});
}
);
_controller.loadPlayers();
}
@override
void dispose() {
_controller.dispose();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
super.dispose();
}
Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top, double sf) {
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 _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),
),
);
}
// 👇 ATIVA O NOVO MAPA DE CALOR 👇
void _showHeatmap(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => HeatmapDialog(
shots: _controller.matchShots,
myTeamName: _controller.myTeam,
oppTeamName: _controller.opponentTeam,
myPlayers: [..._controller.myCourt, ..._controller.myBench],
oppPlayers: [..._controller.oppCourt, ..._controller.oppBench],
playerStats: _controller.playerStats, // Passa os stats para mostrar os pontos
),
);
}
@override
Widget build(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
final double sf = math.min(wScreen / 1150, hScreen / 720);
final double cornerBtnSize = 48 * sf;
if (_controller.isLoading) {
return Scaffold(
backgroundColor: AppTheme.placarDarkSurface,
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: AppTheme.actionPoints.withOpacity(0.7), fontSize: 26 * sf, fontStyle: FontStyle.italic));
},
),
],
),
),
);
}
return Scaffold(
backgroundColor: AppTheme.placarBackground,
body: SafeArea(
top: false,
bottom: false,
child: IgnorePointer(
ignoring: _controller.isSaving,
child: Stack(
children: [
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,
),
),
),
),
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)),
],
if (!_controller.isSelectingShotLocation) ...[
_buildFloatingFoulBtn("FALTA +", AppTheme.actionPoints, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31, sf),
_buildFloatingFoulBtn("FALTA -", AppTheme.actionMiss, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31, sf),
],
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)
),
),
),
),
Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))),
if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller, sf: sf)),
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)),
),
),
),
],
);
},
),
),
Positioned(
top: 50 * sf, left: 12 * sf,
child: _buildCornerBtn(
heroTag: 'btn_save_exit',
icon: Icons.save_alt,
color: AppTheme.oppTeamRed,
size: cornerBtnSize,
isLoading: _controller.isSaving,
onTap: () async {
await _controller.saveGameStats(context);
if (context.mounted) {
Navigator.pop(context);
}
}
),
),
Positioned(
top: 50 * sf, right: 12 * sf,
child: _buildCornerBtn(
heroTag: 'btn_heatmap',
icon: Icons.local_fire_department,
color: Colors.orange.shade800,
size: cornerBtnSize,
onTap: () => _showHeatmap(context),
),
),
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: AppTheme.myTeamBlue, 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 : AppTheme.myTeamBlue,
size: cornerBtnSize,
onTap: _controller.myTimeoutsUsed >= 3
? () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: const Text('🛑 A equipa da casa já usou os 3 Timeouts deste período!'), backgroundColor: AppTheme.actionMiss))
: () => _controller.useTimeout(false)
),
],
),
),
Positioned(
bottom: 55 * sf, right: 12 * sf,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_controller.showOppBench) BenchPlayersList(controller: _controller, isOpponent: true, sf: sf),
SizedBox(height: 12 * sf),
_buildCornerBtn(heroTag: 'btn_sub_away', icon: Icons.swap_horiz, color: AppTheme.oppTeamRed, 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 : AppTheme.oppTeamRed,
size: cornerBtnSize,
onTap: _controller.opponentTimeoutsUsed >= 3
? () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: const Text('🛑 A equipa visitante já usou os 3 Timeouts deste período!'), backgroundColor: AppTheme.actionMiss))
: () => _controller.useTimeout(true)
),
],
),
),
if (_controller.isSaving)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.4),
),
),
],
),
),
),
);
}
}
// ============================================================================
// 👇 O TEU MAPA DE CALOR: ADVERSÁRIO À ESQUERDA | TUA EQUIPA À DIREITA 👇
// ============================================================================
class HeatmapDialog extends StatefulWidget {
final List<ShotRecord> shots;
final String myTeamName;
final String oppTeamName;
final List<String> myPlayers;
final List<String> oppPlayers;
final Map<String, Map<String, int>> playerStats;
const HeatmapDialog({
super.key,
required this.shots,
required this.myTeamName,
required this.oppTeamName,
required this.myPlayers,
required this.oppPlayers,
required this.playerStats,
});
@override
State<HeatmapDialog> createState() => _HeatmapDialogState();
}
class _HeatmapDialogState extends State<HeatmapDialog> {
bool _isMapVisible = false;
String _selectedTeam = '';
String _selectedPlayer = '';
@override
Widget build(BuildContext context) {
final Color headerColor = const Color(0xFFE88F15);
final Color yellowBackground = const Color(0xFFDFAB00);
final double screenHeight = MediaQuery.of(context).size.height;
final double dialogHeight = screenHeight * 0.95;
final double dialogWidth = dialogHeight * 1.0;
return Dialog(
backgroundColor: yellowBackground,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.antiAlias,
insetPadding: const EdgeInsets.all(10),
child: SizedBox(
height: dialogHeight,
width: dialogWidth,
child: _isMapVisible ? _buildMapScreen(headerColor) : _buildSelectionScreen(headerColor),
),
);
}
// ==========================================
// TELA 1: LISTA COM AS DUAS EQUIPAS LADO A LADO
// ==========================================
Widget _buildSelectionScreen(Color headerColor) {
return Column(
children: [
// CABEÇALHO
Container(
height: 40,
color: headerColor,
width: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
const Text(
"ESCOLHE A EQUIPA OU UM JOGADOR",
style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold),
),
Positioned(
right: 8,
child: InkWell(
onTap: () => Navigator.pop(context),
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
child: Icon(Icons.close, color: headerColor, size: 16),
),
),
)
],
),
),
// CORPO: AS DUAS LISTAS LADO A LADO
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
// 👇 ESQUERDA: COLUNA DA EQUIPA ADVERSÁRIA 👇
Expanded(
child: _buildTeamColumn(
teamName: widget.oppTeamName,
players: widget.oppPlayers,
teamColor: AppTheme.oppTeamRed, // Vermelho do Tema
),
),
const SizedBox(width: 8),
// 👇 DIREITA: COLUNA DA TUA EQUIPA 👇
Expanded(
child: _buildTeamColumn(
teamName: widget.myTeamName,
players: widget.myPlayers,
teamColor: AppTheme.myTeamBlue, // Azul do Tema
),
),
],
),
),
),
],
);
}
Widget _buildTeamColumn({required String teamName, required List<String> players, required Color teamColor}) {
List<String> realPlayers = players.where((p) => !p.startsWith("Sem ")).toList();
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
// CABEÇALHO DA EQUIPA (Botão para ver a equipa toda)
InkWell(
onTap: () => setState(() {
_selectedTeam = teamName;
_selectedPlayer = 'Todos';
_isMapVisible = true;
}),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: teamColor,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)),
),
child: Column(
children: [
Text(teamName.toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(12)),
child: const Text("MAPA GERAL DA EQUIPA", style: TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
),
],
),
),
),
// LISTA DOS JOGADORES COM OS SEUS PONTOS
Expanded(
child: ListView.separated(
itemCount: realPlayers.length,
separatorBuilder: (context, index) => const Divider(height: 1, color: Colors.black12),
itemBuilder: (context, index) {
String p = realPlayers[index];
int pts = widget.playerStats[p]?['pts'] ?? 0;
return ListTile(
dense: true,
visualDensity: VisualDensity.compact,
leading: Icon(Icons.person, color: teamColor),
title: Text(p, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)),
trailing: Text("$pts Pts", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: teamColor)),
onTap: () => setState(() {
_selectedTeam = teamName;
_selectedPlayer = p;
_isMapVisible = true; // Abre o mapa para este jogador!
}),
);
},
),
),
],
),
);
}
// ==========================================
// TELA 2: O MAPA DE CALOR DESENHADO
// ==========================================
Widget _buildMapScreen(Color headerColor) {
List<ShotRecord> filteredShots = widget.shots.where((s) {
if (_selectedPlayer != 'Todos') return s.playerName == _selectedPlayer;
if (_selectedTeam == widget.myTeamName) return widget.myPlayers.contains(s.playerName);
if (_selectedTeam == widget.oppTeamName) return widget.oppPlayers.contains(s.playerName);
return true;
}).toList();
String titleText = _selectedPlayer == 'Todos'
? "MAPA GERAL: ${_selectedTeam.toUpperCase()}"
: "MAPA: ${_selectedPlayer.toUpperCase()}";
return Column(
children: [
// CABEÇALHO COM BOTÃO VOLTAR
Container(
height: 40,
color: headerColor,
width: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
left: 8,
child: InkWell(
onTap: () => setState(() => _isMapVisible = false), // Botão de voltar ao menu de seleção!
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
child: Row(
children: [
Icon(Icons.arrow_back, color: headerColor, size: 14),
const SizedBox(width: 4),
Text("VOLTAR", style: TextStyle(color: headerColor, fontWeight: FontWeight.bold, fontSize: 12)),
],
),
),
),
),
Text(
titleText,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold),
),
Positioned(
right: 8,
child: InkWell(
onTap: () => Navigator.pop(context), // Fecha o popup todo
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
child: Icon(Icons.close, color: headerColor, size: 16),
),
),
)
],
),
),
// O DESENHO DO CAMPO E AS BOLAS
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: [
CustomPaint(size: Size(constraints.maxWidth, constraints.maxHeight), painter: HeatmapCourtPainter()),
...filteredShots.map((shot) => Positioned(
left: (shot.relativeX * constraints.maxWidth) - 8,
top: (shot.relativeY * constraints.maxHeight) - 8,
child: CircleAvatar(
radius: 8,
backgroundColor: shot.isMake ? AppTheme.successGreen : AppTheme.actionMiss,
child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white)
),
)),
],
);
},
),
),
],
);
}
}
class HeatmapCourtPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final double w = size.width;
final double h = size.height;
final double basketX = w / 2;
final Paint whiteStroke = Paint()..color = Colors.white..style = PaintingStyle.stroke..strokeWidth = 2.0;
final Paint blackStroke = Paint()..color = Colors.black87..style = PaintingStyle.stroke..strokeWidth = 2.0;
final double margin = w * 0.10;
final double length = h * 0.35;
final double larguraDoArco = (w / 2) - margin;
final double alturaDoArco = larguraDoArco * 0.30;
final double totalArcoHeight = alturaDoArco * 4;
canvas.drawLine(Offset(margin, 0), Offset(margin, length), whiteStroke);
canvas.drawLine(Offset(w - margin, 0), Offset(w - margin, length), whiteStroke);
canvas.drawLine(Offset(0, length), Offset(margin, length), whiteStroke);
canvas.drawLine(Offset(w - margin, length), Offset(w, length), whiteStroke);
canvas.drawArc(Rect.fromCenter(center: Offset(basketX, length), width: larguraDoArco * 2, height: totalArcoHeight), 0, math.pi, false, whiteStroke);
double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75));
double sYL = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.75));
double sXR = basketX + (larguraDoArco * math.cos(math.pi * 0.25));
double sYR = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.25));
canvas.drawLine(Offset(sXL, sYL), Offset(0, h * 0.85), whiteStroke);
canvas.drawLine(Offset(sXR, sYR), Offset(w, h * 0.85), whiteStroke);
final double pW = w * 0.28;
final double pH = h * 0.38;
canvas.drawRect(Rect.fromLTWH(basketX - pW / 2, 0, pW, pH), blackStroke);
final double ftR = pW / 2;
canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), 0, math.pi, false, blackStroke);
for (int i = 0; i < 10; i++) {
canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), math.pi + (i * 2 * (math.pi / 20)), math.pi / 20, false, blackStroke);
}
canvas.drawLine(Offset(basketX - pW / 2, pH), Offset(sXL, sYL), blackStroke);
canvas.drawLine(Offset(basketX + pW / 2, pH), Offset(sXR, sYR), blackStroke);
canvas.drawArc(Rect.fromCircle(center: Offset(basketX, h), radius: w * 0.12), math.pi, math.pi, false, blackStroke);
canvas.drawCircle(Offset(basketX, h * 0.12), w * 0.02, blackStroke);
canvas.drawLine(Offset(basketX - w * 0.08, h * 0.12 - 5), Offset(basketX + w * 0.08, h * 0.12 - 5), blackStroke);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
} }

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA
import '../controllers/register_controller.dart'; import '../controllers/register_controller.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER! import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
@@ -9,16 +10,20 @@ class RegisterHeader extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
Icon(Icons.person_add_outlined, size: 100 * context.sf, color: const Color(0xFFE74C3C)), Icon(Icons.person_add_outlined, size: 100 * context.sf, color: AppTheme.primaryRed), // 👇 Cor do tema
SizedBox(height: 10 * context.sf), SizedBox(height: 10 * context.sf),
Text( Text(
'Nova Conta', 'Nova Conta',
style: TextStyle(fontSize: 36 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[900]), style: TextStyle(
fontSize: 36 * context.sf,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável ao Modo Escuro
),
), ),
SizedBox(height: 5 * context.sf), SizedBox(height: 5 * context.sf),
Text( Text(
'Cria o teu perfil no BasketTrack', 'Cria o teu perfil no BasketTrack',
style: TextStyle(fontSize: 16 * context.sf, color: Colors.grey[600], fontWeight: FontWeight.w500), style: TextStyle(fontSize: 16 * context.sf, color: Colors.grey, fontWeight: FontWeight.w500),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
], ],
@@ -45,12 +50,16 @@ class _RegisterFormFieldsState extends State<RegisterFormFields> {
children: [ children: [
TextFormField( TextFormField(
controller: widget.controller.nameController, controller: widget.controller.nameController,
style: TextStyle(fontSize: 15 * context.sf), style: TextStyle(fontSize: 15 * context.sf, color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Nome Completo', labelText: 'Nome Completo',
labelStyle: TextStyle(fontSize: 15 * context.sf), labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.person_outline, size: 22 * context.sf), prefixIcon: Icon(Icons.person_outline, size: 22 * context.sf, color: AppTheme.primaryRed), // 👇 Cor do tema
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12 * context.sf),
borderSide: BorderSide(color: AppTheme.primaryRed, width: 2), // 👇 Destaque ao focar
),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
), ),
), ),
@@ -59,12 +68,16 @@ class _RegisterFormFieldsState extends State<RegisterFormFields> {
TextFormField( TextFormField(
controller: widget.controller.emailController, controller: widget.controller.emailController,
validator: widget.controller.validateEmail, validator: widget.controller.validateEmail,
style: TextStyle(fontSize: 15 * context.sf), style: TextStyle(fontSize: 15 * context.sf, color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'E-mail', labelText: 'E-mail',
labelStyle: TextStyle(fontSize: 15 * context.sf), labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf), prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf, color: AppTheme.primaryRed),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12 * context.sf),
borderSide: BorderSide(color: AppTheme.primaryRed, width: 2),
),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
), ),
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
@@ -75,13 +88,17 @@ class _RegisterFormFieldsState extends State<RegisterFormFields> {
controller: widget.controller.passwordController, controller: widget.controller.passwordController,
obscureText: _obscurePassword, obscureText: _obscurePassword,
validator: widget.controller.validatePassword, validator: widget.controller.validatePassword,
style: TextStyle(fontSize: 15 * context.sf), style: TextStyle(fontSize: 15 * context.sf, color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Palavra-passe', labelText: 'Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * context.sf), labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf), prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf, color: AppTheme.primaryRed),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12 * context.sf),
borderSide: BorderSide(color: AppTheme.primaryRed, width: 2),
),
suffixIcon: IconButton( 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, size: 22 * context.sf, color: Colors.grey),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword), onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
), ),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
@@ -94,11 +111,15 @@ class _RegisterFormFieldsState extends State<RegisterFormFields> {
controller: widget.controller.confirmPasswordController, controller: widget.controller.confirmPasswordController,
obscureText: _obscurePassword, obscureText: _obscurePassword,
validator: widget.controller.validateConfirmPassword, validator: widget.controller.validateConfirmPassword,
style: TextStyle(fontSize: 15 * context.sf), style: TextStyle(fontSize: 15 * context.sf, color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Confirmar Palavra-passe', labelText: 'Confirmar Palavra-passe',
labelStyle: TextStyle(fontSize: 15 * context.sf), labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.lock_clock_outlined, size: 22 * context.sf), prefixIcon: Icon(Icons.lock_clock_outlined, size: 22 * context.sf, color: AppTheme.primaryRed),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12 * context.sf),
borderSide: BorderSide(color: AppTheme.primaryRed, width: 2),
),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
), ),
@@ -121,7 +142,7 @@ class RegisterButton extends StatelessWidget {
child: ElevatedButton( child: ElevatedButton(
onPressed: controller.isLoading ? null : () => controller.signUp(context), onPressed: controller.isLoading ? null : () => controller.signUp(context),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C), backgroundColor: AppTheme.primaryRed, // 👇 Cor do tema
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
elevation: 3, elevation: 3,

View File

@@ -1,158 +1,67 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/screens/team_stats_page.dart'; import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA
import '../models/team_model.dart'; import '../models/team_model.dart';
import '../controllers/team_controller.dart'; import '../models/person_model.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
class TeamCard extends StatelessWidget { // --- CABEÇALHO ---
class StatsHeader extends StatelessWidget {
final Team team; final Team team;
final TeamController controller;
final VoidCallback onFavoriteTap;
final double sf; // <-- Variável de escala
const TeamCard({ const StatsHeader({super.key, required this.team});
super.key,
required this.team,
required this.controller,
required this.onFavoriteTap,
required this.sf,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Container(
color: Colors.white, padding: EdgeInsets.only(
elevation: 3, top: 50 * context.sf,
margin: EdgeInsets.only(bottom: 12 * sf), left: 20 * context.sf,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)), right: 20 * context.sf,
child: ListTile( bottom: 20 * context.sf
contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf), ),
decoration: BoxDecoration(
// --- 1. IMAGEM + FAVORITO --- color: AppTheme.primaryRed, // 👇 Usando a cor do teu tema!
leading: Stack( borderRadius: BorderRadius.only(
clipBehavior: Clip.none, bottomLeft: Radius.circular(30 * context.sf),
children: [ bottomRight: Radius.circular(30 * context.sf)
CircleAvatar(
radius: 28 * 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 * sf),
)
: null,
),
Positioned(
left: -15 * sf,
top: -10 * sf,
child: IconButton(
icon: Icon(
team.isFavorite ? Icons.star : Icons.star_border,
color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1),
size: 28 * sf,
shadows: [
Shadow(
color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1),
blurRadius: 4 * sf,
),
],
),
onPressed: onFavoriteTap,
),
),
],
),
// --- 2. TÍTULO ---
title: Text(
team.name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf),
overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos
),
// --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) ---
subtitle: Padding(
padding: EdgeInsets.only(top: 6.0 * sf),
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),
initialData: 0,
builder: (context, snapshot) {
final count = snapshot.data ?? 0;
return Text(
"$count Jogs.", // Abreviado para poupar espaço
style: TextStyle(
color: count > 0 ? Colors.green[700] : Colors.orange,
fontWeight: FontWeight.bold,
fontSize: 13 * sf,
),
);
},
),
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,
),
),
],
),
),
// --- 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),
),
],
), ),
), ),
); child: Row(
} children: [
IconButton(
void _confirmDelete(BuildContext context) { icon: Icon(Icons.arrow_back, color: Colors.white, size: 24 * context.sf),
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)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf)),
), ),
TextButton( SizedBox(width: 10 * context.sf),
onPressed: () { CircleAvatar(
controller.deleteTeam(team.id); radius: 24 * context.sf,
Navigator.pop(context); backgroundColor: Colors.white24,
}, backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * sf)), ? NetworkImage(team.imageUrl)
: null,
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
? Text(
team.imageUrl.isEmpty ? "🛡️" : team.imageUrl,
style: TextStyle(fontSize: 20 * context.sf),
)
: null,
),
SizedBox(width: 15 * context.sf),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
team.name,
style: TextStyle(color: Colors.white, fontSize: 20 * context.sf, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
Text(
team.season,
style: TextStyle(color: Colors.white70, fontSize: 14 * context.sf)
),
],
),
), ),
], ],
), ),
@@ -160,90 +69,164 @@ class TeamCard extends StatelessWidget {
} }
} }
// --- DIALOG DE CRIAÇÃO --- // --- CARD DE RESUMO ---
class CreateTeamDialog extends StatefulWidget { class StatsSummaryCard extends StatelessWidget {
final Function(String name, String season, String imageUrl) onConfirm; final int total;
final double sf; // Recebe a escala
const CreateTeamDialog({super.key, required this.onConfirm, required this.sf}); const StatsSummaryCard({super.key, required this.total});
@override
State<CreateTeamDialog> createState() => _CreateTeamDialogState();
}
class _CreateTeamDialogState extends State<CreateTeamDialog> {
final TextEditingController _nameController = TextEditingController();
final TextEditingController _imageController = TextEditingController();
String _selectedSeason = '2024/25';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( // 👇 Adaptável ao Modo Escuro
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)), final cardColor = Theme.of(context).brightness == Brightness.dark
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold)), ? const Color(0xFF1E1E1E)
content: SingleChildScrollView( : Colors.white;
child: Column(
mainAxisSize: MainAxisSize.min, return Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
child: Container(
padding: EdgeInsets.all(20 * context.sf),
decoration: BoxDecoration(
color: cardColor,
borderRadius: BorderRadius.circular(20 * context.sf),
border: Border.all(color: Colors.grey.withOpacity(0.15)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
TextField( Row(
controller: _nameController, children: [
style: TextStyle(fontSize: 14 * widget.sf), Icon(Icons.groups, color: AppTheme.primaryRed, size: 28 * context.sf), // 👇 Cor do tema
decoration: InputDecoration( SizedBox(width: 10 * context.sf),
labelText: 'Nome da Equipa', Text(
labelStyle: TextStyle(fontSize: 14 * widget.sf) "Total de Membros",
), style: TextStyle(
textCapitalization: TextCapitalization.words, color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável
fontSize: 16 * context.sf,
fontWeight: FontWeight.w600
)
),
],
), ),
SizedBox(height: 15 * widget.sf), Text(
DropdownButtonFormField<String>( "$total",
value: _selectedSeason, style: TextStyle(
decoration: InputDecoration( color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável
labelText: 'Temporada', fontSize: 28 * context.sf,
labelStyle: TextStyle(fontSize: 14 * widget.sf) fontWeight: FontWeight.bold
), )
style: TextStyle(fontSize: 14 * widget.sf, color: Colors.black87),
items: ['2023/24', '2024/25', '2025/26']
.map((s) => DropdownMenuItem(value: s, child: Text(s)))
.toList(),
onChanged: (val) => setState(() => _selectedSeason = val!),
),
SizedBox(height: 15 * widget.sf),
TextField(
controller: _imageController,
style: TextStyle(fontSize: 14 * widget.sf),
decoration: InputDecoration(
labelText: 'URL Imagem ou Emoji',
labelStyle: TextStyle(fontSize: 14 * widget.sf),
hintText: 'Ex: 🏀 ou https://...',
hintStyle: TextStyle(fontSize: 14 * widget.sf)
),
), ),
], ],
), ),
), ),
actions: [ );
TextButton( }
onPressed: () => Navigator.pop(context), }
child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf))
), // --- TÍTULO DE SECÇÃO ---
ElevatedButton( class StatsSectionTitle extends StatelessWidget {
style: ElevatedButton.styleFrom( final String title;
backgroundColor: const Color(0xFFE74C3C),
padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf) const StatsSectionTitle({super.key, required this.title});
),
onPressed: () { @override
if (_nameController.text.trim().isNotEmpty) { Widget build(BuildContext context) {
widget.onConfirm( return Column(
_nameController.text.trim(), crossAxisAlignment: CrossAxisAlignment.start,
_selectedSeason, children: [
_imageController.text.trim(), Text(
); title,
Navigator.pop(context); style: TextStyle(
} fontSize: 18 * context.sf,
}, fontWeight: FontWeight.bold,
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)), color: Theme.of(context).colorScheme.onSurface // 👇 Adaptável
)
), ),
Divider(color: Colors.grey.withOpacity(0.3)),
], ],
); );
} }
}
// --- CARD DA PESSOA (JOGADOR/TREINADOR) ---
class PersonCard extends StatelessWidget {
final Person person;
final bool isCoach;
final VoidCallback onEdit;
final VoidCallback onDelete;
const PersonCard({
super.key,
required this.person,
required this.isCoach,
required this.onEdit,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
// 👇 Cores adaptáveis para o Card
final defaultBg = Theme.of(context).brightness == Brightness.dark
? const Color(0xFF1E1E1E)
: Colors.white;
final coachBg = Theme.of(context).brightness == Brightness.dark
? AppTheme.warningAmber.withOpacity(0.1) // Amarelo escuro se for modo noturno
: const Color(0xFFFFF9C4); // Amarelo claro original
return Card(
margin: EdgeInsets.only(top: 12 * context.sf),
elevation: 2,
color: isCoach ? coachBg : defaultBg,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 4 * context.sf),
leading: isCoach
? CircleAvatar(
radius: 22 * context.sf,
backgroundColor: AppTheme.warningAmber, // 👇 Cor do tema
child: Icon(Icons.person, color: Colors.white, size: 24 * context.sf)
)
: Container(
width: 45 * context.sf,
height: 45 * context.sf,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppTheme.primaryRed.withOpacity(0.1), // 👇 Cor do tema
borderRadius: BorderRadius.circular(10 * context.sf)
),
child: Text(
person.number ?? "J",
style: TextStyle(
color: AppTheme.primaryRed, // 👇 Cor do tema
fontWeight: FontWeight.bold,
fontSize: 16 * context.sf
)
),
),
title: Text(
person.name,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16 * context.sf,
color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável
)
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.edit_outlined, color: Colors.blue, size: 22 * context.sf),
onPressed: onEdit,
),
IconButton(
icon: Icon(Icons.delete_outline, color: AppTheme.primaryRed, size: 22 * context.sf), // 👇 Cor do tema
onPressed: onDelete,
),
],
),
),
);
}
} }

228
lib/zone_map_dialog.dart Normal file
View File

@@ -0,0 +1,228 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
class ZoneMapDialog extends StatelessWidget {
final String playerName;
final bool isMake;
final bool is3PointAction; // 👇 AGORA O POP-UP SABE O QUE ARRASTASTE!
final Function(String zone, int points, double relativeX, double relativeY) onZoneSelected;
const ZoneMapDialog({
super.key,
required this.playerName,
required this.isMake,
required this.is3PointAction,
required this.onZoneSelected,
});
@override
Widget build(BuildContext context) {
final Color headerColor = const Color(0xFFE88F15);
final Color yellowBackground = const Color(0xFFDFAB00);
final double screenHeight = MediaQuery.of(context).size.height;
final double dialogHeight = screenHeight * 0.95;
final double dialogWidth = dialogHeight * 1.0;
return Dialog(
backgroundColor: yellowBackground,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.antiAlias,
insetPadding: const EdgeInsets.all(10),
child: SizedBox(
height: dialogHeight,
width: dialogWidth,
child: Column(
children: [
Container(
height: 40,
color: headerColor,
width: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
Text(
isMake ? "Lançamento de $playerName (Marcou)" : "Lançamento de $playerName (Falhou)",
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold),
),
Positioned(
right: 8,
child: InkWell(
onTap: () => Navigator.pop(context),
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
child: Icon(Icons.close, color: headerColor, size: 16),
),
),
)
],
),
),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
return GestureDetector(
onTapUp: (details) => _calculateAndReturnZone(context, details.localPosition, constraints.biggest),
child: CustomPaint(
size: Size(constraints.maxWidth, constraints.maxHeight),
painter: DebugPainter(is3PointAction: is3PointAction), // 👇 Passa a info para o desenhador
),
);
},
),
),
],
),
),
);
}
void _calculateAndReturnZone(BuildContext context, Offset tap, Size size) {
final double w = size.width;
final double h = size.height;
final double x = tap.dx;
final double y = tap.dy;
final double basketX = w / 2;
final double margin = w * 0.10;
final double length = h * 0.35;
final double larguraDoArco = (w / 2) - margin;
final double alturaDoArco = larguraDoArco * 0.30;
final double totalArcoHeight = alturaDoArco * 4;
String zone = "";
int pts = 2;
// 1. SABER SE CLICOU NA ZONA DE 3 OU DE 2
bool is3 = false;
if (y < length) {
if (x < margin || x > w - margin) is3 = true;
} else {
double dx = x - basketX;
double dy = y - length;
double ellipse = (dx * dx) / (larguraDoArco * larguraDoArco) + (dy * dy) / (math.pow(totalArcoHeight / 2, 2));
if (ellipse > 1.0) is3 = true;
}
// 👇 MAGIA AQUI: BLOQUEIA O CLIQUE NA ZONA ESCURA! 👇
if (is3PointAction && !is3) return; // Arrastou 3pts mas clicou na de 2pts -> IGNORA
if (!is3PointAction && is3) return; // Arrastou 2pts mas clicou na de 3pts -> IGNORA
double angle = math.atan2(y - length, x - basketX);
if (is3) {
pts = 3;
if (y < length) {
zone = (x < w / 2) ? "Canto Esquerdo (3pt)" : "Canto Direito (3pt)";
} else if (angle > 2.35) {
zone = "Ala Esquerda (3pt)";
} else if (angle < 0.78) {
zone = "Ala Direita (3pt)";
} else {
zone = "Topo (3pt)";
}
} else {
pts = 2;
final double pW = w * 0.28;
final double pH = h * 0.38;
if (x > basketX - pW / 2 && x < basketX + pW / 2 && y < pH) {
zone = "Garrafão";
} else {
if (y < length) {
zone = (x < w / 2) ? "Meia Distância (Canto Esq)" : "Meia Distância (Canto Dir)";
} else if (angle > 2.35) {
zone = "Meia Distância (Esq)";
} else if (angle < 0.78) {
zone = "Meia Distância (Dir)";
} else {
zone = "Meia Distância (Centro)";
}
}
}
// 👇 A MUDANÇA ESTÁ AQUI! Passamos os dados e deixamos quem chamou decidir como fechar!
onZoneSelected(zone, pts, x / w, y / h);
}
}
class DebugPainter extends CustomPainter {
final bool is3PointAction;
DebugPainter({required this.is3PointAction});
@override
void paint(Canvas canvas, Size size) {
final double w = size.width;
final double h = size.height;
final double basketX = w / 2;
final Paint whiteStroke = Paint()..color = Colors.white..style = PaintingStyle.stroke..strokeWidth = 2.0;
final Paint blackStroke = Paint()..color = Colors.black87..style = PaintingStyle.stroke..strokeWidth = 2.0;
final double margin = w * 0.10;
final double length = h * 0.35;
final double larguraDoArco = (w / 2) - margin;
final double alturaDoArco = larguraDoArco * 0.30;
final double totalArcoHeight = alturaDoArco * 4;
// DESENHA O CAMPO
canvas.drawLine(Offset(margin, 0), Offset(margin, length), whiteStroke);
canvas.drawLine(Offset(w - margin, 0), Offset(w - margin, length), whiteStroke);
canvas.drawLine(Offset(0, length), Offset(margin, length), whiteStroke);
canvas.drawLine(Offset(w - margin, length), Offset(w, length), whiteStroke);
canvas.drawArc(Rect.fromCenter(center: Offset(basketX, length), width: larguraDoArco * 2, height: totalArcoHeight), 0, math.pi, false, whiteStroke);
double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75));
double sYL = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.75));
double sXR = basketX + (larguraDoArco * math.cos(math.pi * 0.25));
double sYR = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.25));
canvas.drawLine(Offset(sXL, sYL), Offset(0, h * 0.85), whiteStroke);
canvas.drawLine(Offset(sXR, sYR), Offset(w, h * 0.85), whiteStroke);
final double pW = w * 0.28;
final double pH = h * 0.38;
canvas.drawRect(Rect.fromLTWH(basketX - pW / 2, 0, pW, pH), blackStroke);
final double ftR = pW / 2;
canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), 0, math.pi, false, blackStroke);
for (int i = 0; i < 10; i++) {
canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), math.pi + (i * 2 * (math.pi / 20)), math.pi / 20, false, blackStroke);
}
canvas.drawLine(Offset(basketX - pW / 2, pH), Offset(sXL, sYL), blackStroke);
canvas.drawLine(Offset(basketX + pW / 2, pH), Offset(sXR, sYR), blackStroke);
canvas.drawArc(Rect.fromCircle(center: Offset(basketX, h), radius: w * 0.12), math.pi, math.pi, false, blackStroke);
canvas.drawCircle(Offset(basketX, h * 0.12), w * 0.02, blackStroke);
canvas.drawLine(Offset(basketX - w * 0.08, h * 0.12 - 5), Offset(basketX + w * 0.08, h * 0.12 - 5), blackStroke);
// ==========================================
// 👇 EFEITO DE ESCURECIMENTO (SHADOW) 👇
// ==========================================
final Paint shadowPaint = Paint()..color = Colors.black.withOpacity(0.75); // 75% escuro!
// Cria o molde da área de 2 pontos
Path path2pt = Path();
path2pt.moveTo(margin, 0);
path2pt.lineTo(margin, length);
// Faz o arco curvo da linha de 3 pontos
path2pt.arcTo(Rect.fromCenter(center: Offset(basketX, length), width: larguraDoArco * 2, height: totalArcoHeight), math.pi, -math.pi, false);
path2pt.lineTo(w - margin, 0);
path2pt.close();
if (is3PointAction) {
// Arrastou 3 Pontos -> Escurece a Zona de 2!
canvas.drawPath(path2pt, shadowPaint);
} else {
// Arrastou 2 Pontos -> Escurece a Zona de 3!
Path fullScreen = Path()..addRect(Rect.fromLTWH(0, 0, w, h));
Path path3pt = Path.combine(PathOperation.difference, fullScreen, path2pt);
canvas.drawPath(path3pt, shadowPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

View File

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