From ec5bdc4867d11edd3bfbd8a4024390dc823abc92 Mon Sep 17 00:00:00 2001 From: 230404 <230404@epvc.pt> Date: Mon, 16 Mar 2026 23:25:48 +0000 Subject: [PATCH] git lixo 2 --- lib/controllers/game_controller.dart | 36 +- lib/controllers/home_controller.dart | 3 - lib/controllers/team_controller.dart | 18 +- .../controllers/contollers_grafico.dart | 4 +- lib/grafico de pizza/dados_grafico.dart | 1 + lib/grafico de pizza/grafico.dart | 268 ++++++------ .../widgets/grafico_widgets.dart | 95 ++-- lib/pages/gamePage.dart | 173 ++++---- lib/pages/home.dart | 74 +++- lib/pages/teamPage.dart | 289 +++++++------ lib/utils/size_extension.dart | 12 +- lib/widgets/game_widgets.dart | 292 ++----------- lib/widgets/home_widgets.dart | 62 +-- lib/widgets/placar_widgets.dart | 314 ++++++++------ lib/widgets/team_widgets.dart | 405 +++++++++--------- 15 files changed, 974 insertions(+), 1072 deletions(-) diff --git a/lib/controllers/game_controller.dart b/lib/controllers/game_controller.dart index ffa141a..c0e5b6c 100644 --- a/lib/controllers/game_controller.dart +++ b/lib/controllers/game_controller.dart @@ -5,7 +5,7 @@ class GameController { final _supabase = Supabase.instance.client; // 1. LER JOGOS (Stream em Tempo Real) -Stream> get gamesStream { + Stream> get gamesStream { return _supabase .from('games') // 1. Fica à escuta da tabela original (Garante o Tempo Real!) .stream(primaryKey: ['id']) @@ -21,6 +21,40 @@ Stream> get gamesStream { 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> getFilteredGames({required String teamFilter, required String seasonFilter}) { + return _supabase + .from('games') + .stream(primaryKey: ['id']) + .asyncMap((event) async { + + // 1. Começamos a query APENAS com o select (Sem o order ainda!) + var query = _supabase.from('games_with_logos').select(); + + // 2. Se a temporada não for "Todas", aplicamos o filtro AQUI + if (seasonFilter != 'Todas') { + query = query.eq('season', seasonFilter); + } + + // 3. Executamos a query e aplicamos o ORDER BY no final + final viewData = await query.order('game_date', ascending: false); + + List 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(); + } + + return games; + }); + } // 2. CRIAR JOGO // Retorna o ID do jogo criado para podermos navegar para o placar Future createGame(String myTeam, String opponent, String season) async { diff --git a/lib/controllers/home_controller.dart b/lib/controllers/home_controller.dart index 63f3f74..5848d54 100644 --- a/lib/controllers/home_controller.dart +++ b/lib/controllers/home_controller.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; class HomeController extends ChangeNotifier { - // Se precisar de estado para a home screen int _selectedCardIndex = 0; int get selectedCardIndex => _selectedCardIndex; @@ -11,10 +10,8 @@ class HomeController extends ChangeNotifier { notifyListeners(); } - // Métodos adicionais para lógica da home void navigateToDetails(String playerName) { print('Navegando para detalhes de $playerName'); - // Implementar navegação } void refreshData() { diff --git a/lib/controllers/team_controller.dart b/lib/controllers/team_controller.dart index bf45320..3d27f08 100644 --- a/lib/controllers/team_controller.dart +++ b/lib/controllers/team_controller.dart @@ -5,7 +5,6 @@ class TeamController { final _supabase = Supabase.instance.client; // 1. STREAM (Realtime) - // Adicionei o .map() no final para garantir que o Dart entende que é uma List Stream>> get teamsStream { return _supabase .from('teams') @@ -15,7 +14,6 @@ class TeamController { } // 2. CRIAR - // Alterei imageUrl para String? (pode ser nulo) para evitar erros se não houver imagem Future createTeam(String name, String season, String? imageUrl) async { try { await _supabase.from('teams').insert({ @@ -51,13 +49,12 @@ class TeamController { } } - // 5. CONTAR JOGADORES - // CORRIGIDO: A sintaxe antiga dava erro. O método .count() é o correto agora. + // 5. CONTAR JOGADORES (LEITURA ÚNICA) Future getPlayerCount(String teamId) async { try { final count = await _supabase .from('members') - .count() // Retorna diretamente o número inteiro + .count() .eq('team_id', teamId); return count; } catch (e) { @@ -66,6 +63,17 @@ class TeamController { } } + // 👇 6. A FUNÇÃO QUE RESOLVE O ERRO (EM TEMPO REAL) 👇 + Stream getPlayerCountStream(String teamId) { + return _supabase + .from('members') + .stream(primaryKey: ['id']) + .eq('team_id', teamId) + .map((membros) => membros + .where((membro) => membro['type'] == 'Jogador') + .length); + } + // Mantemos o dispose vazio para não quebrar a chamada na TeamsPage void dispose() {} } \ No newline at end of file diff --git a/lib/grafico de pizza/controllers/contollers_grafico.dart b/lib/grafico de pizza/controllers/contollers_grafico.dart index fa11e46..7aae400 100644 --- a/lib/grafico de pizza/controllers/contollers_grafico.dart +++ b/lib/grafico de pizza/controllers/contollers_grafico.dart @@ -1,5 +1,5 @@ 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 { PieChartData _chartData = const PieChartData(victories: 0, defeats: 0, draws: 0); @@ -10,7 +10,7 @@ class PieChartController extends ChangeNotifier { _chartData = PieChartData( victories: victories ?? _chartData.victories, defeats: defeats ?? _chartData.defeats, - draws: draws ?? _chartData.draws, // 👇 AGORA ELE ACEITA OS EMPATES + draws: draws ?? _chartData.draws, ); notifyListeners(); } diff --git a/lib/grafico de pizza/dados_grafico.dart b/lib/grafico de pizza/dados_grafico.dart index 17b131f..8b559f0 100644 --- a/lib/grafico de pizza/dados_grafico.dart +++ b/lib/grafico de pizza/dados_grafico.dart @@ -22,5 +22,6 @@ class PieChartData { 'total': total, 'victoryPercentage': victoryPercentage, 'defeatPercentage': defeatPercentage, + 'drawPercentage': drawPercentage, }; } \ No newline at end of file diff --git a/lib/grafico de pizza/grafico.dart b/lib/grafico de pizza/grafico.dart index eb7a40e..95b5a6c 100644 --- a/lib/grafico de pizza/grafico.dart +++ b/lib/grafico de pizza/grafico.dart @@ -1,21 +1,28 @@ 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 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA ADICIONADO PARA USARMOS O primaryRed +import 'dart:math' as math; class PieChartCard extends StatefulWidget { - final PieChartController? controller; + final int victories; + final int defeats; + final int draws; final String title; final String subtitle; final Color? backgroundColor; final VoidCallback? onTap; + final double sf; const PieChartCard({ super.key, - this.controller, + this.victories = 0, + this.defeats = 0, + this.draws = 0, this.title = 'DESEMPENHO', this.subtitle = 'Temporada', this.onTap, - required this.backgroundColor, + this.backgroundColor, this.sf = 1.0, }); @@ -24,30 +31,26 @@ class PieChartCard extends StatefulWidget { } class _PieChartCardState extends State with SingleTickerProviderStateMixin { - late PieChartController _controller; late AnimationController _animationController; late Animation _animation; @override void initState() { super.initState(); - _controller = widget.controller ?? PieChartController(); - - _animationController = AnimationController( - duration: const Duration(milliseconds: 600), - vsync: this, - ); - - _animation = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation( - parent: _animationController, - curve: Curves.easeOutBack, - ), - ); - + _animationController = AnimationController(duration: const Duration(milliseconds: 600), vsync: this); + _animation = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeOutBack)); _animationController.forward(); } + @override + void didUpdateWidget(PieChartCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.victories != widget.victories || oldWidget.defeats != widget.defeats || oldWidget.draws != widget.draws) { + _animationController.reset(); + _animationController.forward(); + } + } + @override void dispose() { _animationController.dispose(); @@ -58,30 +61,31 @@ class _PieChartCardState extends State with SingleTickerProviderSt Widget build(BuildContext context) { 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, builder: (context, child) { return Transform.scale( - // O scale pode passar de 1.0 (efeito back), mas a opacidade NÃO scale: 0.95 + (_animation.value * 0.05), - child: Opacity( - // 👇 AQUI ESTÁ A FIX: Garante que fica entre 0 e 1 - opacity: _animation.value.clamp(0.0, 1.0), - child: child, - ), + child: Opacity(opacity: _animation.value.clamp(0.0, 1.0), child: child), ); }, child: Card( margin: EdgeInsets.zero, - elevation: 4, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + elevation: 0, // Ajustado para não ter sombra dupla, já que o tema pode ter + 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( onTap: widget.onTap, - borderRadius: BorderRadius.circular(14), child: Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [widget.backgroundColor.withOpacity(0.9), widget.backgroundColor.withOpacity(0.7)]), + color: cardColor, // 👇 APLICA A COR BLINDADA ), child: LayoutBuilder( builder: (context, constraints) { @@ -89,161 +93,147 @@ class _PieChartCardState extends State with SingleTickerProviderSt final double cw = constraints.maxWidth; return Padding( - padding: EdgeInsets.all(cw * 0.06), + padding: EdgeInsets.symmetric(horizontal: cw * 0.05, vertical: ch * 0.03), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 👇 TÍTULOS UM POUCO MAIS PRESENTES + // --- CABEÇALHO --- (👇 MANTIDO ALINHADO À ESQUERDA) FittedBox( fit: BoxFit.scaleDown, - child: Text(widget.title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), letterSpacing: 1.0)), + 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( - fit: BoxFit.scaleDown, - child: Text(widget.subtitle, style: TextStyle(fontSize: ch * 0.07, fontWeight: FontWeight.bold, color: Colors.white)), + Text(widget.subtitle, + style: TextStyle( + fontSize: ch * 0.055, + fontWeight: FontWeight.bold, + color: AppTheme.backgroundLight, // 👇 USANDO O TEU backgroundLight + ) ), - SizedBox(height: ch * 0.03), + const Expanded(flex: 1, child: SizedBox()), - // MEIO (GRÁFICO + ESTATÍSTICAS) - Expanded( + // --- MIOLO (GRÁFICO MAIOR À ESQUERDA + STATS) --- + Expanded( + flex: 9, child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, // Changed from spaceBetween to end to push stats more to the right children: [ - Expanded( - flex: 1, + // 1. Lado Esquerdo: Donut Chart + // 👇 MUDANÇA AQUI: Gráfico ainda maior! cw * 0.52 + SizedBox( + width: cw * 0.52, + height: cw * 0.52, child: PieChartWidget( victoryPercentage: data.victoryPercentage, defeatPercentage: data.defeatPercentage, 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( - flex: 1, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, ch), - _buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.yellow, ch), - _buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, ch), - _buildDynDivider(ch), - _buildDynStatRow("TOT", data.total.toString(), "100", Colors.white, ch), - ], + child: FittedBox( + alignment: Alignment.centerRight, // Encosta os números à direita + fit: BoxFit.scaleDown, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, // Alinha os números à direita para ficar arrumado + children: [ + _buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, textColor, ch, cw), + _buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.amber, textColor, ch, cw), + _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 - SizedBox(height: ch * 0.03), + + const Expanded(flex: 1, child: SizedBox()), + + // --- RODAPÉ: BOTÃO WIN RATE GIGANTE --- (👇 MUDANÇA AQUI: Alinhado à esquerda) Container( width: double.infinity, - padding: EdgeInsets.symmetric(vertical: ch * 0.035), + padding: EdgeInsets.symmetric(vertical: ch * 0.025), decoration: BoxDecoration( - color: Colors.white24, // Igual ao fundo do botão detalhes - borderRadius: BorderRadius.circular(ch * 0.03), // Borda arredondada + color: textColor.withOpacity(0.05), // 👇 Fundo adaptável + borderRadius: BorderRadius.circular(12), ), - child: Center( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - data.victoryPercentage >= 0.5 ? Icons.trending_up : Icons.trending_down, - color: Colors.green, - size: ch * 0.09 - ), - SizedBox(width: cw * 0.02), - Text( - 'WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%', - style: TextStyle( - fontSize: ch * 0.05, - fontWeight: FontWeight.bold, - color: Colors.white - ) + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, // 👇 MUDANÇA AQUI: Letras mais para a esquerda! + children: [ + Icon(Icons.stars, color: Colors.green, size: ch * 0.075), + const SizedBox(width: 10), + Text('WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%', + style: TextStyle( + color: AppTheme.backgroundLight, + fontWeight: FontWeight.w900, + letterSpacing: 1.0, + fontSize: ch * 0.06 ), ), ], ), - ], + ), ), - ), - - SizedBox(height: 10), // Espaço controlado - - // Win rate - Sempre visível e não sobreposto - Container( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - data.victoryPercentage > 0.5 - ? Icons.trending_up - : Icons.trending_down, - color: data.victoryPercentage > 0.5 - ? Colors.green - : Colors.red, - size: 18, // Pequeno - ), - SizedBox(width: 8), - Text( - 'Win Rate: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%', - style: TextStyle( - fontSize: 14, // Pequeno - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ], - ), - ), - ], - ), - ), + ], + ), + ); + } ), ), ), ), ); } - // 👇 PERCENTAGENS SUBIDAS LIGEIRAMENTE (0.10 e 0.045) - Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch) { + + // 👇 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( - padding: EdgeInsets.only(bottom: ch * 0.01), + padding: EdgeInsets.symmetric(vertical: ch * 0.005), child: Row( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Número subiu para 0.10 - Expanded(flex: 2, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(number, style: TextStyle(fontSize: ch * 0.10, fontWeight: FontWeight.bold, color: color, height: 1.0)))), - SizedBox(width: ch * 0.02), - Expanded( - flex: 3, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Row(children: [ - Container(width: ch * 0.018, height: ch * 0.018, margin: EdgeInsets.only(right: ch * 0.015), decoration: BoxDecoration(color: color, shape: BoxShape.circle)), - // Label subiu para 0.045 - Expanded(child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(label, style: TextStyle(fontSize: ch * 0.033, color: Colors.white.withOpacity(0.8), fontWeight: FontWeight.w600)))) - ]), - // Percentagem subiu para 0.05 - FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('$percent%', style: TextStyle(fontSize: ch * 0.04, color: color, fontWeight: FontWeight.bold))), - ]), + SizedBox( + width: cw * 0.12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Text(label, style: TextStyle(fontSize: ch * 0.045, color: textColor.withOpacity(0.6), fontWeight: FontWeight.bold)), // 👇 TEXTO ADAPTÁVEL (increased from 0.035) + Text('$percent%', style: TextStyle(fontSize: ch * 0.05, color: statColor, fontWeight: FontWeight.bold)), // (increased from 0.04) + ], + ), ), + 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) { - return Container(height: 0.5, color: Colors.white.withOpacity(0.1), margin: EdgeInsets.symmetric(vertical: ch * 0.01)); + Widget _buildDynDivider(double cw, Color textColor) { + return Container( + width: cw * 0.35, + height: 1.5, + color: textColor.withOpacity(0.2), // 👇 LINHA ADAPTÁVEL + margin: const EdgeInsets.symmetric(vertical: 4) + ); } } \ No newline at end of file diff --git a/lib/grafico de pizza/widgets/grafico_widgets.dart b/lib/grafico de pizza/widgets/grafico_widgets.dart index 1a56ed2..d7ac90f 100644 --- a/lib/grafico de pizza/widgets/grafico_widgets.dart +++ b/lib/grafico de pizza/widgets/grafico_widgets.dart @@ -5,26 +5,23 @@ class PieChartWidget extends StatelessWidget { final double victoryPercentage; final double defeatPercentage; final double drawPercentage; - final double size; + final double sf; const PieChartWidget({ super.key, required this.victoryPercentage, required this.defeatPercentage, this.drawPercentage = 0, - this.size = 140, // Aumentado para 400x300 + required this.sf, }); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - // 👇 MAGIA ANTI-DESAPARECIMENTO 👇 - // Vê o espaço real. Se por algum motivo for infinito, assume 100 para não sumir. final double w = constraints.maxWidth.isInfinite ? 100.0 : constraints.maxWidth; final double h = constraints.maxHeight.isInfinite ? 100.0 : constraints.maxHeight; - // Pega no menor valor para garantir que o círculo não é cortado final double size = math.min(w, h); return Center( @@ -32,7 +29,7 @@ class PieChartWidget extends StatelessWidget { width: size, height: size, child: CustomPaint( - painter: _PieChartPainter( + painter: _DonutChartPainter( victoryPercentage: victoryPercentage, defeatPercentage: defeatPercentage, drawPercentage: drawPercentage, @@ -48,24 +45,27 @@ class PieChartWidget extends StatelessWidget { } Widget _buildCenterLabels(double size) { + final bool hasGames = victoryPercentage > 0 || defeatPercentage > 0 || drawPercentage > 0; + return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - '${(victoryPercentage * 100).toStringAsFixed(1)}%', + // 👇 Casa decimal aplicada aqui! + hasGames ? '${(victoryPercentage * 100).toStringAsFixed(1)}%' : '---', 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, - color: Colors.white, + color: hasGames ? Colors.white : Colors.white54, ), ), SizedBox(height: size * 0.02), Text( - 'Vitórias', + hasGames ? 'Vitórias' : 'Sem Jogos', style: TextStyle( - fontSize: size * 0.10, - color: Colors.white.withOpacity(0.8), + fontSize: size * 0.08, + color: hasGames ? Colors.white70 : Colors.white38, ), ), ], @@ -87,59 +87,40 @@ class _DonutChartPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); - // Margem de 5% para a linha de fora não ser cortada - final radius = (size.width / 2) - (size.width * 0.05); + final radius = (size.width / 2) - (size.width * 0.1); + 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 defeatColor = Colors.red; - const drawColor = Colors.yellow; - const borderColor = Colors.white30; - + const drawColor = Colors.amber; + 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) { - final paint = Paint() - ..color = color - ..style = PaintingStyle.fill; - - 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) + void drawDonutSector(double percentage, Color color) { + if (percentage <= 0) return; + final sweepAngle = 2 * math.pi * percentage; + final paint = Paint() + ..color = color ..style = PaintingStyle.stroke - ..strokeWidth = totalWidth * 0.015; + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.butt; - final lineX = center.dx + radius * math.cos(startAngle); - final lineY = center.dy + radius * math.sin(startAngle); - - canvas.drawLine(center, Offset(lineX, lineY), linePaint); + canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, paint); + startAngle += sweepAngle; } + + drawDonutSector(victoryPercentage, victoryColor); + drawDonutSector(drawPercentage, drawColor); + drawDonutSector(defeatPercentage, defeatColor); } @override diff --git a/lib/pages/gamePage.dart b/lib/pages/gamePage.dart index a1f8113..fcd733d 100644 --- a/lib/pages/gamePage.dart +++ b/lib/pages/gamePage.dart @@ -1,77 +1,91 @@ import 'package:flutter/material.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/game_controller.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 --- class GameResultCard extends StatelessWidget { final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season; - final String? myTeamLogo, opponentTeamLogo; + final String? myTeamLogo, opponentTeamLogo; + final double sf; const GameResultCard({ super.key, required this.gameId, required this.myTeam, required this.opponentTeam, required this.myScore, required this.opponentScore, required this.status, required this.season, - this.myTeamLogo, this.opponentTeamLogo, + this.myTeamLogo, this.opponentTeamLogo, required this.sf, }); @override Widget build(BuildContext context) { + final bgColor = Theme.of(context).cardTheme.color; + final textColor = Theme.of(context).colorScheme.onSurface; + return Container( - margin: EdgeInsets.only(bottom: 16 * context.sf), - padding: EdgeInsets.all(16 * context.sf), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * context.sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * context.sf)]), + margin: EdgeInsets.only(bottom: 16 * sf), + padding: EdgeInsets.all(16 * 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( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: _buildTeamInfo(context, myTeam, const Color(0xFFE74C3C), myTeamLogo)), - _buildScoreCenter(context, gameId), - Expanded(child: _buildTeamInfo(context, opponentTeam, Colors.black87, opponentTeamLogo)), + Expanded(child: _buildTeamInfo(myTeam, AppTheme.primaryRed, myTeamLogo, sf, textColor)), + _buildScoreCenter(context, gameId, sf, textColor), + 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( children: [ - CircleAvatar(radius: 24 * context.sf, backgroundColor: color, backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * context.sf) : null), - SizedBox(height: 6 * context.sf), - Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2), + CircleAvatar( + radius: 24 * sf, + 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( children: [ Row( mainAxisSize: MainAxisSize.min, children: [ - _scoreBox(context, myScore, Colors.green), - Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * context.sf)), - _scoreBox(context, opponentScore, Colors.grey), + _scoreBox(myScore, AppTheme.successGreen, sf), + Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf, color: textColor)), + _scoreBox(opponentScore, Colors.grey, sf), ], ), - SizedBox(height: 10 * context.sf), + SizedBox(height: 10 * sf), TextButton.icon( onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))), - icon: Icon(Icons.play_circle_fill, size: 18 * context.sf, color: const Color(0xFFE74C3C)), - label: Text("RETORNAR", style: TextStyle(fontSize: 11 * context.sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)), - style: TextButton.styleFrom(backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), padding: EdgeInsets.symmetric(horizontal: 14 * context.sf, vertical: 8 * context.sf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), visualDensity: VisualDensity.compact), + icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: AppTheme.primaryRed), + label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: AppTheme.primaryRed, fontWeight: FontWeight.bold)), + 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), - Text(status, style: TextStyle(fontSize: 12 * context.sf, color: Colors.blue, fontWeight: FontWeight.bold)), + SizedBox(height: 6 * sf), + Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)), ], ); } - Widget _scoreBox(BuildContext context, String pts, Color c) => Container( - padding: EdgeInsets.symmetric(horizontal: 12 * context.sf, vertical: 6 * context.sf), - decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * context.sf)), - child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)), + Widget _scoreBox(String pts, Color c, double sf) => Container( + padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf), + decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)), + child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)), ); } @@ -79,8 +93,9 @@ class GameResultCard extends StatelessWidget { class CreateGameDialogManual extends StatefulWidget { final TeamController teamController; 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 State createState() => _CreateGameDialogManualState(); @@ -106,24 +121,29 @@ class _CreateGameDialogManualState extends State { @override Widget build(BuildContext context) { return AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), - title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)), + backgroundColor: Theme.of(context).colorScheme.surface, + 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( child: Column( mainAxisSize: MainAxisSize.min, children: [ - TextField(controller: _seasonController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * context.sf))), - SizedBox(height: 15 * context.sf), + TextField( + 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), - 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), ], ), ), 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( - 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 { if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) { setState(() => _isLoading = true); @@ -135,7 +155,7 @@ class _CreateGameDialogManualState extends State { } } }, - 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)), ), ], ); @@ -157,9 +177,10 @@ class _CreateGameDialogManualState extends State { return Align( alignment: Alignment.topLeft, 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( - 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( padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, itemBuilder: (BuildContext context, int index) { @@ -167,8 +188,8 @@ class _CreateGameDialogManualState extends State { final String name = option['name'].toString(); final String? imageUrl = option['image_url']; return ListTile( - leading: CircleAvatar(radius: 20 * context.sf, backgroundColor: Colors.grey.shade200, backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * context.sf) : null), - title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), + 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 * widget.sf, color: Theme.of(context).colorScheme.onSurface)), onTap: () { onSelected(option); }, ); }, @@ -181,8 +202,9 @@ class _CreateGameDialogManualState extends State { if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) txtCtrl.text = controller.text; txtCtrl.addListener(() { controller.text = txtCtrl.text; }); return TextField( - controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * context.sf), - decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * context.sf), prefixIcon: Icon(Icons.search, size: 20 * context.sf), border: const OutlineInputBorder()), + controller: txtCtrl, focusNode: node, + 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)), ); }, ); @@ -202,20 +224,24 @@ class GamePage extends StatefulWidget { class _GamePageState extends State { final GameController gameController = GameController(); final TeamController teamController = TeamController(); + String selectedSeason = 'Todas'; + String selectedTeam = 'Todas'; @override Widget build(BuildContext context) { + bool isFilterActive = selectedSeason != 'Todas' || selectedTeam != 'Todas'; + return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)), - backgroundColor: Colors.white, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, elevation: 0, actions: [ Padding( padding: EdgeInsets.only(right: 8.0 * context.sf), child: IconButton( - icon: Icon(isFilterActive ? Icons.filter_list_alt : Icons.filter_list, color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87, size: 26 * context.sf), + 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), ), ) @@ -225,40 +251,28 @@ class _GamePageState extends State { stream: teamController.teamsStream, builder: (context, teamSnapshot) { final List> teamsList = teamSnapshot.data ?? []; - - // 2º STREAM: Lemos os jogos return StreamBuilder>( - stream: gameController.gamesStream, + stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason), builder: (context, gameSnapshot) { if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) return const Center(child: CircularProgressIndicator()); - if (gameSnapshot.hasError) return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * context.sf))); + if (gameSnapshot.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) { - 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( - padding: const EdgeInsets.all(16), + padding: EdgeInsets.all(16 * context.sf), itemCount: gameSnapshot.data!.length, itemBuilder: (context, index) { final game = gameSnapshot.data![index]; - - // --- LÓGICA PARA ENCONTRAR A IMAGEM PELO NOME --- - String? myLogo; - String? oppLogo; - + String? myLogo, oppLogo; for (var team in teamsList) { - if (team['name'] == game.myTeam) { - myLogo = team['image_url']; - } - if (team['name'] == game.opponentTeam) { - oppLogo = team['image_url']; - } + if (team['name'] == game.myTeam) myLogo = team['image_url']; + if (team['name'] == game.opponentTeam) oppLogo = team['image_url']; } - - // Agora já passamos as imagens para o cartão! return GameResultCard( gameId: game.id, myTeam: game.myTeam, opponentTeam: game.opponentTeam, myScore: game.myScore, opponentScore: game.opponentScore, status: game.status, season: game.season, myTeamLogo: myLogo, opponentTeamLogo: oppLogo, + sf: context.sf, ); }, ); @@ -267,49 +281,53 @@ class _GamePageState extends State { }, ), floatingActionButton: FloatingActionButton( - heroTag: 'add_game_btn', // 👇 A MÁGICA ESTÁ AQUI TAMBÉM! - backgroundColor: const Color(0xFFE74C3C), + heroTag: 'add_game_btn', + backgroundColor: AppTheme.primaryRed, 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)), ), ); } - void _showCreateDialog(BuildContext context) { + void _showFilterPopup(BuildContext context) { + String tempSeason = selectedSeason; + String tempTeam = selectedTeam; showDialog( context: context, builder: (context) { return StatefulBuilder( builder: (context, setPopupState) { return AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)), + 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()) ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, 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), 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: DropdownButton( - 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(value: value, child: Text(value))).toList(), onChanged: (newValue) => setPopupState(() => tempSeason = newValue!), ), ), ), SizedBox(height: 20 * context.sf), - Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), + Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)), SizedBox(height: 6 * context.sf), Container( - padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)), + 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>>( stream: teamController.teamsStream, builder: (context, snapshot) { @@ -318,7 +336,8 @@ class _GamePageState extends State { if (!teamNames.contains(tempTeam)) tempTeam = 'Todas'; return DropdownButtonHideUnderline( child: DropdownButton( - 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(value: value, child: Text(value, overflow: TextOverflow.ellipsis))).toList(), onChanged: (newValue) => setPopupState(() => tempTeam = newValue!), ), @@ -330,7 +349,7 @@ class _GamePageState extends State { ), actions: [ TextButton(onPressed: () { setState(() { selectedSeason = 'Todas'; selectedTeam = 'Todas'; }); Navigator.pop(context); }, child: Text('LIMPAR', style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey))), - ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), onPressed: () { setState(() { selectedSeason = tempSeason; selectedTeam = tempTeam; }); Navigator.pop(context); }, child: Text('APLICAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * context.sf))), + 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))), ], ); } diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 53fb32a..afd09ed 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -8,6 +8,8 @@ import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:playmaker/pages/status_page.dart'; import '../utils/size_extension.dart'; import 'settings_screen.dart'; +// 👇 Importa o ficheiro onde meteste o StatCard e o SportGrid +// import 'home_widgets.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -38,7 +40,7 @@ class _HomeScreenState extends State { ]; return Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, // Fundo dinâmico + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)), backgroundColor: AppTheme.primaryRed, @@ -46,7 +48,6 @@ class _HomeScreenState extends State { leading: IconButton( icon: Icon(Icons.person, size: 24 * context.sf), onPressed: () { - // 👇 MAGIA ACONTECE AQUI 👇 Navigator.push( context, MaterialPageRoute(builder: (context) => const SettingsScreen()), @@ -80,7 +81,7 @@ class _HomeScreenState extends State { void _showTeamSelector(BuildContext context) { showModalBottomSheet( context: context, - backgroundColor: Theme.of(context).colorScheme.surface, // Fundo dinâmico + backgroundColor: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf))), builder: (context) { return StreamBuilder>>( @@ -96,7 +97,7 @@ class _HomeScreenState extends State { itemBuilder: (context, index) { final team = teams[index]; return ListTile( - title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), // Texto dinâmico + title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), onTap: () { setState(() { _selectedTeamId = team['id']; @@ -195,11 +196,55 @@ class _HomeScreenState extends State { Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: textColor)), SizedBox(height: 16 * context.sf), + // 👇 AQUI ESTÁ O NOVO CARTÃO VAZIO PARA QUANDO NÃO HÁ EQUIPA 👇 _selectedTeamName == "Selecionar Equipa" ? Container( - padding: EdgeInsets.all(20 * context.sf), - alignment: Alignment.center, - child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)), + width: double.infinity, + padding: EdgeInsets.all(24.0 * 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>>( stream: _supabase.from('games').stream(primaryKey: ['id']) @@ -248,17 +293,10 @@ class _HomeScreenState extends State { if (myScore < oppScore) result = 'D'; return _buildGameHistoryCard( - context: context, - opponent: opponent, - result: result, - myScore: myScore, - oppScore: oppScore, - date: date, - topPts: game['top_pts_name'] ?? '---', - topAst: game['top_ast_name'] ?? '---', - topRbs: game['top_rbs_name'] ?? '---', - topDef: game['top_def_name'] ?? '---', - mvp: game['mvp_name'] ?? '---', + context: context, opponent: opponent, result: result, + myScore: myScore, oppScore: oppScore, date: date, + topPts: game['top_pts_name'] ?? '---', topAst: game['top_ast_name'] ?? '---', + topRbs: game['top_rbs_name'] ?? '---', topDef: game['top_def_name'] ?? '---', mvp: game['mvp_name'] ?? '---', ); }).toList(), ); diff --git a/lib/pages/teamPage.dart b/lib/pages/teamPage.dart index 1e13a12..23b3484 100644 --- a/lib/pages/teamPage.dart +++ b/lib/pages/teamPage.dart @@ -3,7 +3,7 @@ import 'package:playmaker/screens/team_stats_page.dart'; import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA import '../controllers/team_controller.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 { const TeamsPage({super.key}); @@ -26,7 +26,6 @@ class _TeamsPageState extends State { super.dispose(); } - // --- POPUP DE FILTROS --- void _showFilterDialog(BuildContext context) { showDialog( context: context, @@ -34,14 +33,14 @@ class _TeamsPageState extends State { return StatefulBuilder( builder: (context, setModalState) { return AlertDialog( - backgroundColor: const Color(0xFF2C3E50), + backgroundColor: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, 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( - 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), ) ], @@ -49,12 +48,11 @@ class _TeamsPageState extends State { content: Column( mainAxisSize: MainAxisSize.min, children: [ - const Divider(color: Colors.white24), + Divider(color: Colors.grey.withOpacity(0.2)), SizedBox(height: 16 * context.sf), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Coluna Temporada Expanded( child: _buildPopupColumn( title: "TEMPORADA", @@ -66,8 +64,7 @@ class _TeamsPageState extends State { }, ), ), - const SizedBox(width: 20), - // Coluna Ordenar + SizedBox(width: 20 * context.sf), Expanded( child: _buildPopupColumn( title: "ORDENAR POR", @@ -86,7 +83,7 @@ class _TeamsPageState extends State { actions: [ TextButton( 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)), ), ], ); @@ -96,28 +93,24 @@ class _TeamsPageState extends State { ); } - Widget _buildPopupColumn({ - required String title, - required List options, - required String currentValue, - required Function(String) onSelect, - }) { + Widget _buildPopupColumn({required String title, required List options, required String currentValue, required Function(String) onSelect}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: const TextStyle(color: Colors.grey, fontSize: 11, fontWeight: FontWeight.bold)), - const SizedBox(height: 12), + Text(title, style: TextStyle(color: Colors.grey, fontSize: 11 * context.sf, fontWeight: FontWeight.bold)), + SizedBox(height: 12 * context.sf), ...options.map((opt) { final isSelected = currentValue == opt; return InkWell( onTap: () => onSelect(opt), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), + padding: EdgeInsets.symmetric(vertical: 8.0 * context.sf), child: Text( opt, 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, + fontSize: 14 * context.sf, ), ), ), @@ -133,11 +126,11 @@ class _TeamsPageState extends State { backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( title: Text("Minhas Equipas", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)), - backgroundColor: const Color(0xFFF5F7FA), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, elevation: 0, actions: [ 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), ), ], @@ -149,8 +142,8 @@ class _TeamsPageState extends State { ], ), floatingActionButton: FloatingActionButton( - heroTag: 'add_team_btn', // 👇 A MÁGICA ESTÁ AQUI! - backgroundColor: const Color(0xFFE74C3C), + heroTag: 'add_team_btn', + backgroundColor: AppTheme.primaryRed, child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf), onPressed: () => _showCreateDialog(context), ), @@ -159,17 +152,17 @@ class _TeamsPageState extends State { Widget _buildSearchBar() { return Padding( - padding: const EdgeInsets.all(16.0), + padding: EdgeInsets.all(16.0 * context.sf), child: TextField( controller: _searchController, 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( hintText: 'Pesquisar equipa...', - hintStyle: TextStyle(fontSize: 16 * context.sf), - prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * context.sf), + hintStyle: TextStyle(fontSize: 16 * context.sf, color: Colors.grey), + prefixIcon: Icon(Icons.search, color: AppTheme.primaryRed, size: 22 * context.sf), 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), ), ), @@ -180,51 +173,30 @@ class _TeamsPageState extends State { return StreamBuilder>>( stream: controller.teamsStream, builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator()); - if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * context.sf))); + if (snapshot.connectionState == ConnectionState.waiting) return 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, color: Theme.of(context).colorScheme.onSurface))); var data = List>.from(snapshot.data!); - // --- 1. FILTROS --- - if (_selectedSeason != 'Todas') { - data = data.where((t) => t['season'] == _selectedSeason).toList(); - } - if (_searchQuery.isNotEmpty) { - data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList(); - } + if (_selectedSeason != 'Todas') data = data.where((t) => t['season'] == _selectedSeason).toList(); + if (_searchQuery.isNotEmpty) data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList(); - // --- 2. ORDENAÇÃO (FAVORITOS PRIMEIRO) --- data.sort((a, b) { - // Apanhar o estado de favorito (tratando null como false) bool favA = a['is_favorite'] ?? false; bool favB = b['is_favorite'] ?? false; - - // REGRA 1: Favoritos aparecem sempre primeiro - if (favA && !favB) return -1; // A sobe - if (!favA && favB) return 1; // B sobe - - // REGRA 2: Se o estado de favorito for igual, aplica o filtro do utilizador - if (_currentSort == 'Nome') { - return a['name'].toString().compareTo(b['name'].toString()); - } else { // Recentes - return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString()); - } + if (favA && !favB) return -1; + if (!favA && favB) return 1; + if (_currentSort == 'Nome') return a['name'].toString().compareTo(b['name'].toString()); + else return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString()); }); return ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: EdgeInsets.symmetric(horizontal: 16 * context.sf), itemCount: data.length, itemBuilder: (context, index) { final team = Team.fromMap(data[index]); - - // Navegação para estatísticas return GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => TeamStatsPage(team: team)), - ); - }, + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))), child: TeamCard( team: team, controller: controller, @@ -239,7 +211,7 @@ class _TeamsPageState extends State { } 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))); } } @@ -248,73 +220,140 @@ class TeamCard extends StatelessWidget { final Team team; final TeamController controller; 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 Widget build(BuildContext context) { - return Card( - color: Colors.white, elevation: 3, margin: EdgeInsets.only(bottom: 12 * context.sf), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)), - child: ListTile( - contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 8 * context.sf), - leading: Stack( - clipBehavior: Clip.none, - children: [ - CircleAvatar( - radius: 28 * context.sf, backgroundColor: Colors.grey[200], - backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http')) ? NetworkImage(team.imageUrl) : null, - child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http')) ? Text(team.imageUrl.isEmpty ? "🏀" : team.imageUrl, style: TextStyle(fontSize: 24 * context.sf)) : null, - ), - Positioned( - left: -15 * context.sf, top: -10 * context.sf, - child: IconButton( - icon: Icon(team.isFavorite ? Icons.star : Icons.star_border, color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1), size: 28 * context.sf, shadows: [Shadow(color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), blurRadius: 4 * context.sf)]), - onPressed: onFavoriteTap, - ), - ), - ], - ), - title: Text(team.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * context.sf), overflow: TextOverflow.ellipsis), - subtitle: Padding( - padding: EdgeInsets.only(top: 6.0 * context.sf), - child: Row( + final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface; + final textColor = Theme.of(context).colorScheme.onSurface; + + return Container( + margin: EdgeInsets.only(bottom: 12 * sf), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(15 * sf), + border: Border.all(color: Colors.grey.withOpacity(0.15)), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10 * sf)] + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(15 * sf), + child: ListTile( + contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf), + leading: Stack( + clipBehavior: Clip.none, children: [ - Icon(Icons.groups_outlined, size: 16 * context.sf, color: Colors.grey), - SizedBox(width: 4 * context.sf), - StreamBuilder( - stream: controller.getPlayerCountStream(team.id), - initialData: 0, - builder: (context, snapshot) { - final count = snapshot.data ?? 0; - return Text("$count Jogs.", style: TextStyle(color: count > 0 ? Colors.green[700] : Colors.orange, fontWeight: FontWeight.bold, fontSize: 13 * context.sf)); - }, + CircleAvatar( + radius: 28 * sf, + backgroundColor: Colors.grey.withOpacity(0.2), + 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 ? 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( + 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( context: context, builder: (context) => AlertDialog( - title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), - content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * context.sf)), + backgroundColor: cardColor, + 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: [ - TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))), - TextButton(onPressed: () { controller.deleteTeam(team.id); Navigator.pop(context); }, child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * context.sf))), + TextButton( + 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)), + ), ], ), ); @@ -324,7 +363,9 @@ class TeamCard extends StatelessWidget { // --- DIALOG DE CRIAÇÃO --- class CreateTeamDialog extends StatefulWidget { 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 State createState() => _CreateTeamDialogState(); @@ -338,31 +379,33 @@ class _CreateTeamDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)), - title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), + backgroundColor: Theme.of(context).colorScheme.surface, + 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( child: Column( mainAxisSize: MainAxisSize.min, children: [ - TextField(controller: _nameController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * context.sf)), textCapitalization: TextCapitalization.words), - SizedBox(height: 15 * context.sf), + 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 * widget.sf), DropdownButtonFormField( - value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf)), - style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87), + dropdownColor: Theme.of(context).colorScheme.surface, + 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(), onChanged: (val) => setState(() => _selectedSeason = val!), ), - SizedBox(height: 15 * context.sf), - TextField(controller: _imageController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'URL Imagem ou Emoji', labelStyle: TextStyle(fontSize: 14 * context.sf), hintText: 'Ex: 🏀 ou https://...', hintStyle: TextStyle(fontSize: 14 * context.sf))), + SizedBox(height: 15 * widget.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: [ - 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( - 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); } }, - child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * context.sf)), + child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)), ), ], ); diff --git a/lib/utils/size_extension.dart b/lib/utils/size_extension.dart index b9f78bb..793bc2c 100644 --- a/lib/utils/size_extension.dart +++ b/lib/utils/size_extension.dart @@ -1 +1,11 @@ -// TODO Implement this library. \ No newline at end of file +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +extension SizeExtension on BuildContext { + double get sf { + final double wScreen = MediaQuery.of(this).size.width; + final double hScreen = MediaQuery.of(this).size.height; + // Ajusta a escala baseada no ecrã (muda os valores 1150/720 conforme a tua calibração) + return math.min(wScreen / 1150, hScreen / 720); + } +} \ No newline at end of file diff --git a/lib/widgets/game_widgets.dart b/lib/widgets/game_widgets.dart index 5f723a8..c073376 100644 --- a/lib/widgets/game_widgets.dart +++ b/lib/widgets/game_widgets.dart @@ -1,27 +1,18 @@ import 'package:flutter/material.dart'; import 'package:playmaker/pages/PlacarPage.dart'; +import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA! import '../controllers/team_controller.dart'; import '../controllers/game_controller.dart'; class GameResultCard extends StatelessWidget { - final String gameId; - final String myTeam, opponentTeam, myScore, opponentScore, status, season; - final String? myTeamLogo; - final String? opponentTeamLogo; - final double sf; // NOVA VARIÁVEL DE ESCALA + final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season; + final String? myTeamLogo, opponentTeamLogo; + final double sf; const GameResultCard({ - super.key, - required this.gameId, - required this.myTeam, - required this.opponentTeam, - required this.myScore, - required this.opponentScore, - required this.status, - required this.season, - this.myTeamLogo, - this.opponentTeamLogo, - required this.sf, // OBRIGATÓRIO RECEBER A ESCALA + super.key, required this.gameId, required this.myTeam, required this.opponentTeam, + required this.myScore, required this.opponentScore, required this.status, required this.season, + this.myTeamLogo, this.opponentTeamLogo, required this.sf, }); @override @@ -31,291 +22,76 @@ class GameResultCard extends StatelessWidget { final textColor = Theme.of(context).colorScheme.onSurface; return Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(16), + margin: EdgeInsets.only(bottom: 16 * sf), + padding: EdgeInsets.all(16 * sf), decoration: BoxDecoration( - color: Colors.white, + color: bgColor, // Usa a cor do tema borderRadius: BorderRadius.circular(20 * sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, 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), - 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( children: [ CircleAvatar( - radius: 24 * sf, // Ajuste do tamanho do logo + radius: 24 * sf, 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, + backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, + child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * sf) : null, ), - const SizedBox(height: 4), + SizedBox(height: 6 * sf), Text(name, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 2, // Permite 2 linhas para nomes compridos não cortarem + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf, color: textColor), // Adapta à noite/dia + textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2, ), ], ); } Widget _buildScoreCenter(BuildContext context, String id, double sf) { + final textColor = Theme.of(context).colorScheme.onSurface; + return Column( children: [ Row( mainAxisSize: MainAxisSize.min, children: [ - _scoreBox(myScore, Colors.green, sf), - Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf)), + _scoreBox(myScore, AppTheme.successGreen, sf), // Verde do tema + Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf, color: textColor)), _scoreBox(opponentScore, Colors.grey, sf), ], ), - const SizedBox(height: 8), + SizedBox(height: 10 * sf), TextButton.icon( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PlacarPage( - gameId: id, - myTeam: myTeam, - opponentTeam: opponentTeam, - ), - ), - ); - }, - icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: const Color(0xFFE74C3C)), - label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)), + onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))), + icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: AppTheme.primaryRed), + label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: AppTheme.primaryRed, fontWeight: FontWeight.bold)), 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), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), visualDensity: VisualDensity.compact, ), ), - const SizedBox(height: 4), - Text(status, style: const TextStyle(fontSize: 10, color: Colors.blue, fontWeight: FontWeight.bold)), + SizedBox(height: 6 * sf), + Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)), ], ); } - Widget _scoreBox(String pts, Color c) => Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8)), - child: Text(pts, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + Widget _scoreBox(String pts, Color c, double sf) => Container( + padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf), + decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)), + child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)), ); -} - -// --- 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 createState() => _CreateGameDialogManualState(); -} - -class _CreateGameDialogManualState extends State { - 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>>( - stream: widget.teamController.teamsStream, - builder: (context, snapshot) { - List> teamList = snapshot.hasData ? snapshot.data! : []; - - return Autocomplete>( - displayStringForOption: (Map option) => option['name'].toString(), - - optionsBuilder: (TextEditingValue val) { - if (val.text.isEmpty) return const Iterable>.empty(); - return teamList.where((t) => - t['name'].toString().toLowerCase().contains(val.text.toLowerCase())); - }, - - onSelected: (Map 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() - ), - ); - }, - ); - }, - ); - } } \ No newline at end of file diff --git a/lib/widgets/home_widgets.dart b/lib/widgets/home_widgets.dart index 04b3f48..5553137 100644 --- a/lib/widgets/home_widgets.dart +++ b/lib/widgets/home_widgets.dart @@ -25,7 +25,7 @@ class StatCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return SizedBox( width: HomeConfig.cardwidthPadding, height: HomeConfig.cardheightPadding, child: Card( @@ -33,7 +33,7 @@ class StatCard extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: isHighlighted - ? BorderSide(color: Colors.amber, width: 2) + ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none, ), child: InkWell( @@ -68,7 +68,7 @@ class StatCard extends StatelessWidget { title.toUpperCase(), style: HomeConfig.titleStyle, ), - SizedBox(height: 5), + const SizedBox(height: 5), Text( playerName, style: HomeConfig.playerNameStyle, @@ -80,22 +80,16 @@ class StatCard extends StatelessWidget { ), if (isHighlighted) Container( - padding: EdgeInsets.all(8), - decoration: BoxDecoration( + padding: const EdgeInsets.all(8), + decoration: const BoxDecoration( color: Colors.amber, shape: BoxShape.circle, ), - child: Icon( - Icons.star, - size: 20, - color: Colors.white, - ), + child: const Icon(Icons.star, size: 20, color: Colors.white), ), ], ), - - SizedBox(height: 10), - + const SizedBox(height: 10), // Ícone Container( width: 60, @@ -104,51 +98,32 @@ class StatCard extends StatelessWidget { color: Colors.white.withOpacity(0.2), shape: BoxShape.circle, ), - child: Icon( - icon, - size: 30, - color: Colors.white, - ), + child: Icon(icon, size: 30, color: Colors.white), ), - - Spacer(), - + const Spacer(), // Estatística Center( child: Column( children: [ - Text( - statValue, - style: HomeConfig.statValueStyle, - ), - SizedBox(height: 5), - Text( - statLabel.toUpperCase(), - style: HomeConfig.statLabelStyle, - ), + Text(statValue, style: HomeConfig.statValueStyle), + const SizedBox(height: 5), + Text(statLabel.toUpperCase(), style: HomeConfig.statLabelStyle), ], ), ), - - Spacer(), - + const Spacer(), // Botão Container( width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 12), + padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(15), ), - child: Center( + child: const Center( child: Text( 'VER DETALHES', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 14, - letterSpacing: 1, - ), + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, letterSpacing: 1), ), ), ), @@ -174,11 +149,10 @@ class SportGrid extends StatelessWidget { @override Widget build(BuildContext context) { - if (children.isEmpty) return SizedBox(); + if (children.isEmpty) return const SizedBox(); return Column( children: [ - // Primeira linha if (children.length >= 2) Padding( padding: EdgeInsets.only(bottom: spacing), @@ -191,8 +165,6 @@ class SportGrid extends StatelessWidget { ], ), ), - - // Segunda linha if (children.length >= 4) Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/widgets/placar_widgets.dart b/lib/widgets/placar_widgets.dart index 4f5cd37..ad3d5c0 100644 --- a/lib/widgets/placar_widgets.dart +++ b/lib/widgets/placar_widgets.dart @@ -7,70 +7,104 @@ import 'package:playmaker/zone_map_dialog.dart'; // ============================================================================ class TopScoreboard extends StatelessWidget { final PlacarController controller; - const TopScoreboard({super.key, required this.controller}); + final double sf; + + const TopScoreboard({super.key, required this.controller, required this.sf}); @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 30), + padding: EdgeInsets.symmetric(vertical: 10 * sf, horizontal: 35 * sf), decoration: BoxDecoration( - color: const Color(0xFF16202C), - borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(15), bottomRight: Radius.circular(15)), - border: Border.all(color: Colors.white, width: 2), + color: const Color(0xFF16202C), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(22 * sf), + bottomRight: Radius.circular(22 * sf) + ), + border: Border.all(color: Colors.white, width: 2.5 * sf), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - _buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false), - const SizedBox(width: 25), + _buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false, sf), + SizedBox(width: 30 * sf), Column( + mainAxisSize: MainAxisSize.min, children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6), - decoration: BoxDecoration(color: const Color(0xFF2C3E50), borderRadius: BorderRadius.circular(6)), - child: Text(controller.formatTime(), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, fontFamily: 'monospace')), + padding: EdgeInsets.symmetric(horizontal: 18 * sf, vertical: 5 * sf), + decoration: BoxDecoration( + color: const Color(0xFF2C3E50), + borderRadius: BorderRadius.circular(9 * sf) + ), + child: Text( + controller.formatTime(), + style: TextStyle(color: Colors.white, fontSize: 28 * sf, fontWeight: FontWeight.w900, fontFamily: 'monospace', letterSpacing: 2 * sf) + ), + ), + SizedBox(height: 5 * sf), + Text( + "PERÍODO ${controller.currentQuarter}", + style: TextStyle(color: Colors.orangeAccent, fontSize: 14 * sf, fontWeight: FontWeight.w900) ), - const SizedBox(height: 5), - Text("PERÍODO ${controller.currentQuarter}", style: const TextStyle(color: Colors.orangeAccent, fontSize: 14, fontWeight: FontWeight.bold)), ], ), - const SizedBox(width: 25), - _buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true), + SizedBox(width: 30 * sf), + _buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true, sf), ], ), ); } - Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp) { + Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp, double sf) { + int displayFouls = fouls > 5 ? 5 : fouls; + final timeoutIndicators = Row( mainAxisSize: MainAxisSize.min, children: List.generate(3, (index) => Container( - margin: const EdgeInsets.symmetric(horizontal: 3), - width: 12, height: 12, - decoration: BoxDecoration(shape: BoxShape.circle, color: index < timeouts ? Colors.yellow : Colors.grey.shade600, border: Border.all(color: Colors.black26)), + margin: EdgeInsets.symmetric(horizontal: 3.5 * sf), + width: 12 * sf, height: 12 * sf, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: index < timeouts ? Colors.yellow : Colors.grey.shade600, + border: Border.all(color: Colors.white54, width: 1.5 * sf) + ), )), ); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: isOpp - ? [ - Column(children: [_scoreBox(score, color), const SizedBox(height: 4), Text("FALTAS: $fouls", style: TextStyle(color: fouls >= 5 ? Colors.red : Colors.yellowAccent, fontSize: 12, fontWeight: FontWeight.bold)), timeoutIndicators]), - const SizedBox(width: 15), - Text(name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)) - ] - : [ - Text(name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), - const SizedBox(width: 15), - Column(children: [_scoreBox(score, color), const SizedBox(height: 4), Text("FALTAS: $fouls", style: TextStyle(color: fouls >= 5 ? Colors.red : Colors.yellowAccent, fontSize: 12, fontWeight: FontWeight.bold)), timeoutIndicators]) - ] - ); + + List content = [ + Column( + children: [ + _scoreBox(score, color, sf), + SizedBox(height: 7 * sf), + timeoutIndicators + ] + ), + SizedBox(width: 18 * sf), + Column( + crossAxisAlignment: isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end, + children: [ + Text( + name.toUpperCase(), + style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.2 * sf) + ), + SizedBox(height: 5 * sf), + Text( + "FALTAS: $displayFouls", + style: TextStyle(color: displayFouls >= 5 ? Colors.redAccent : Colors.yellowAccent, fontSize: 13 * sf, fontWeight: FontWeight.bold) + ), + ], + ) + ]; + + return Row(crossAxisAlignment: CrossAxisAlignment.center, children: isOpp ? content : content.reversed.toList()); } - Widget _scoreBox(int score, Color color) => Container( - width: 50, height: 40, + Widget _scoreBox(int score, Color color, double sf) => Container( + width: 58 * sf, height: 45 * sf, alignment: Alignment.center, - decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(6)), - child: Text(score.toString(), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)), + decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(7 * sf)), + child: Text(score.toString(), style: TextStyle(color: Colors.white, fontSize: 26 * sf, fontWeight: FontWeight.w900)), ); } @@ -80,7 +114,9 @@ class TopScoreboard extends StatelessWidget { class BenchPlayersList extends StatelessWidget { final PlacarController controller; final bool isOpponent; - const BenchPlayersList({super.key, required this.controller, required this.isOpponent}); + final double sf; + + const BenchPlayersList({super.key, required this.controller, required this.isOpponent, required this.sf}); @override Widget build(BuildContext context) { @@ -96,24 +132,45 @@ class BenchPlayersList extends StatelessWidget { final bool isFouledOut = fouls >= 5; Widget avatarUI = Container( - margin: const EdgeInsets.only(bottom: 5), + margin: EdgeInsets.only(bottom: 7 * sf), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 1.8 * sf), + boxShadow: [BoxShadow(color: Colors.black45, blurRadius: 5 * sf, offset: Offset(0, 2.5 * sf))] + ), child: CircleAvatar( - backgroundColor: isFouledOut ? Colors.grey.shade700 : teamColor, - child: Text(num, style: TextStyle(color: isFouledOut ? Colors.red.shade300 : Colors.white, fontSize: 14, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), + radius: 22 * sf, + backgroundColor: isFouledOut ? Colors.grey.shade800 : teamColor, + child: Text( + num, + style: TextStyle( + color: isFouledOut ? Colors.red.shade300 : Colors.white, + fontSize: 16 * sf, + fontWeight: FontWeight.bold, + decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none + ) + ), ), ); if (isFouledOut) { return GestureDetector( - onTap: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $playerName não pode voltar (Expulso).'), backgroundColor: Colors.red)), - child: avatarUI, + onTap: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $playerName não pode voltar (Expulso).'), backgroundColor: Colors.red)), + child: avatarUI ); } return Draggable( data: "$prefix$playerName", - feedback: Material(color: Colors.transparent, child: CircleAvatar(backgroundColor: teamColor, child: Text(num, style: const TextStyle(color: Colors.white)))), - childWhenDragging: const Opacity(opacity: 0.5, child: SizedBox(width: 40, height: 40)), + feedback: Material( + color: Colors.transparent, + child: CircleAvatar( + radius: 28 * sf, + backgroundColor: teamColor, + child: Text(num, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18 * sf)) + ) + ), + childWhenDragging: Opacity(opacity: 0.5, child: SizedBox(width: 45 * sf, height: 45 * sf)), child: avatarUI, ); }).toList(), @@ -131,8 +188,9 @@ class PlayerCourtCard extends StatelessWidget { final PlacarController controller; final String name; final bool isOpponent; + final double sf; - const PlayerCourtCard({super.key, required this.controller, required this.name, required this.isOpponent}); + const PlayerCourtCard({super.key, required this.controller, required this.name, required this.isOpponent, required this.sf}); @override Widget build(BuildContext context) { @@ -146,12 +204,12 @@ class PlayerCourtCard extends StatelessWidget { feedback: Material( color: Colors.transparent, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(8)), - child: Text(name, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), + padding: EdgeInsets.symmetric(horizontal: 18 * sf, vertical: 11 * sf), + decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(9 * sf)), + child: Text(name, style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)), ), ), - childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, name, stats, teamColor, false, false)), + childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, name, stats, teamColor, false, false, sf)), child: DragTarget( onAcceptWithDetails: (details) { final action = details.data; @@ -177,30 +235,27 @@ class PlayerCourtCard extends StatelessWidget { // Se for 1 Ponto (Lance Livre), Falta, Ressalto ou Roubo, FAZ TUDO NORMAL! else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) { controller.handleActionDrag(context, action, "$prefix$name"); - } - else if (action.startsWith("bench_")) { + } else if (action.startsWith("bench_")) { controller.handleSubbing(context, action, name, isOpponent); } }, builder: (context, candidateData, rejectedData) { bool isSubbing = candidateData.any((data) => data != null && (data.startsWith("bench_my_") || data.startsWith("bench_opp_"))); bool isActionHover = candidateData.any((data) => data != null && (data.startsWith("add_") || data.startsWith("sub_") || data.startsWith("miss_"))); - return _playerCardUI(number, name, stats, teamColor, isSubbing, isActionHover); + return _playerCardUI(number, name, stats, teamColor, isSubbing, isActionHover, sf); }, ), ); } Widget _playerCardUI(String number, String name, Map stats, Color teamColor, bool isSubbing, bool isActionHover, double sf) { + // ... (Mantém o teu código de design _playerCardUI que já tinhas aqui dentro, fica igualzinho!) bool isFouledOut = stats["fls"]! >= 5; - Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white; + Color bgColor = isFouledOut ? Colors.red.shade50 : Colors.white; Color borderColor = isFouledOut ? Colors.redAccent : Colors.transparent; - if (isSubbing) { - bgColor = Colors.blue.shade50; borderColor = Colors.blue; - } else if (isActionHover && !isFouledOut) { - bgColor = Colors.orange.shade50; borderColor = Colors.orange; - } + if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = Colors.blue; } + else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = Colors.orange; } int fgm = stats["fgm"]!; int fga = stats["fga"]!; @@ -208,10 +263,11 @@ class PlayerCourtCard extends StatelessWidget { String displayName = name.length > 12 ? "${name.substring(0, 10)}..." : name; return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( - color: bgColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: borderColor, width: 2), - boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 3))], + color: bgColor, + borderRadius: BorderRadius.circular(11 * sf), + border: Border.all(color: borderColor, width: 1.8 * sf), + boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5 * sf, offset: Offset(2 * sf, 3.5 * sf))], ), child: ClipRRect( borderRadius: BorderRadius.circular(9 * sf), @@ -232,19 +288,10 @@ class PlayerCourtCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text( - displayName, - style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? Colors.red : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none) - ), + Text(displayName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? Colors.red : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), SizedBox(height: 2.5 * sf), - Text( - "${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", - style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[700], fontWeight: FontWeight.w600) - ), - Text( - "${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls", - style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[500], fontWeight: FontWeight.w600) - ), + Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[700], fontWeight: FontWeight.w600)), + Text("${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls", style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[500], fontWeight: FontWeight.w600)), ], ), ), @@ -261,12 +308,14 @@ class PlayerCourtCard extends StatelessWidget { // ============================================================================ class ActionButtonsPanel extends StatelessWidget { final PlacarController controller; - const ActionButtonsPanel({super.key, required this.controller}); + final double sf; + + const ActionButtonsPanel({super.key, required this.controller, required this.sf}); @override Widget build(BuildContext context) { - final double baseSize = 65 * sf; // Reduzido (Antes era 75) - final double feedSize = 82 * sf; // Reduzido (Antes era 95) + final double baseSize = 65 * sf; + final double feedSize = 82 * sf; final double gap = 7 * sf; return Row( @@ -274,116 +323,119 @@ class ActionButtonsPanel extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ _columnBtn([ - _actionBtn("T.O", const Color(0xFF1E5BB2), () => controller.useTimeout(false), labelSize: 20), - _dragAndTargetBtn("1", Colors.orange, "add_pts_1"), - _dragAndTargetBtn("1", Colors.orange, "sub_pts_1", isX: true), - _dragAndTargetBtn("STL", Colors.green, "add_stl"), - - ]), - const SizedBox(width: 15), + _dragAndTargetBtn("M1", Colors.redAccent, "miss_1", baseSize, feedSize, sf), + _dragAndTargetBtn("1", Colors.orange, "add_pts_1", baseSize, feedSize, sf), + _dragAndTargetBtn("1", Colors.orange, "sub_pts_1", baseSize, feedSize, sf, isX: true), + _dragAndTargetBtn("STL", Colors.green, "add_stl", baseSize, feedSize, sf), + ], gap), + SizedBox(width: gap * 1), _columnBtn([ - _dragAndTargetBtn("M2", Colors.redAccent, "miss_2"), - _dragAndTargetBtn("2", Colors.orange, "add_pts_2"), - _dragAndTargetBtn("2", Colors.orange, "sub_pts_2", isX: true), - _dragAndTargetBtn("AST", Colors.blueGrey, "add_ast"), - ]), - const SizedBox(width: 15), + _dragAndTargetBtn("M2", Colors.redAccent, "miss_2", baseSize, feedSize, sf), + _dragAndTargetBtn("2", Colors.orange, "add_pts_2", baseSize, feedSize, sf), + _dragAndTargetBtn("2", Colors.orange, "sub_pts_2", baseSize, feedSize, sf, isX: true), + _dragAndTargetBtn("AST", Colors.blueGrey, "add_ast", baseSize, feedSize, sf), + ], gap), + SizedBox(width: gap * 1), _columnBtn([ - _dragAndTargetBtn("M3", Colors.redAccent, "miss_3"), - _dragAndTargetBtn("3", Colors.orange, "add_pts_3"), - _dragAndTargetBtn("3", Colors.orange, "sub_pts_3", isX: true), - _dragAndTargetBtn("TOV", Colors.redAccent, "add_tov"), - ]), - const SizedBox(width: 15), + _dragAndTargetBtn("M3", Colors.redAccent, "miss_3", baseSize, feedSize, sf), + _dragAndTargetBtn("3", Colors.orange, "add_pts_3", baseSize, feedSize, sf), + _dragAndTargetBtn("3", Colors.orange, "sub_pts_3", baseSize, feedSize, sf, isX: true), + _dragAndTargetBtn("TOV", Colors.redAccent, "add_tov", baseSize, feedSize, sf), + ], gap), + SizedBox(width: gap * 1), _columnBtn([ - _actionBtn("T.O", const Color(0xFFD92C2C), () => controller.useTimeout(true), labelSize: 20), - _dragAndTargetBtn("ORB", const Color(0xFF1E2A38), "add_rbs", icon: Icons.sports_basketball), - _dragAndTargetBtn("DRB", const Color(0xFF1E2A38), "add_rbs", icon: Icons.sports_basketball), - - _dragAndTargetBtn("BLK", Colors.deepPurple, "add_blk", icon: Icons.front_hand), - ]), - const SizedBox(width: 15), - _columnBtn([ - ]) + _dragAndTargetBtn("ORB", const Color(0xFF1E2A38), "add_orb", baseSize, feedSize, sf, icon: Icons.sports_basketball), + _dragAndTargetBtn("DRB", const Color(0xFF1E2A38), "add_drb", baseSize, feedSize, sf, icon: Icons.sports_basketball), + _dragAndTargetBtn("BLK", Colors.deepPurple, "add_blk", baseSize, feedSize, sf, icon: Icons.front_hand), + ], gap), ], ); } - // Mantenha os métodos _columnBtn, _dragAndTargetBtn, _actionBtn e _circle exatamente como estão - Widget _columnBtn(List children) => Column(mainAxisSize: MainAxisSize.min, children: children.map((c) => Padding(padding: const EdgeInsets.only(bottom: 8), child: c)).toList()); + Widget _columnBtn(List children, double gap) { + return Column( + mainAxisSize: MainAxisSize.min, + children: children.map((c) => Padding(padding: EdgeInsets.only(bottom: gap), child: c)).toList() + ); + } - Widget _dragAndTargetBtn(String label, Color color, String actionData, {IconData? icon, bool isX = false}) { + Widget _dragAndTargetBtn(String label, Color color, String actionData, double baseSize, double feedSize, double sf, {IconData? icon, bool isX = false}) { return Draggable( data: actionData, - feedback: _circle(label, color, icon, true, isX: isX), - childWhenDragging: Opacity(opacity: 0.5, child: _circle(label, color, icon, false, isX: isX)), + feedback: _circle(label, color, icon, true, baseSize, feedSize, sf, isX: isX), + childWhenDragging: Opacity( + opacity: 0.5, + child: _circle(label, color, icon, false, baseSize, feedSize, sf, isX: isX) + ), child: DragTarget( - onAcceptWithDetails: (details) {}, + onAcceptWithDetails: (details) {}, // O PlayerCourtCard é que processa a ação! builder: (context, candidateData, rejectedData) { bool isHovered = candidateData.any((data) => data != null && data.startsWith("player_")); return Transform.scale( scale: isHovered ? 1.15 : 1.0, - child: Container(decoration: isHovered ? BoxDecoration(shape: BoxShape.circle, boxShadow: const [BoxShadow(color: Colors.white, blurRadius: 10, spreadRadius: 3)]) : null, child: _circle(label, color, icon, false, isX: isX)), + child: Container( + decoration: isHovered ? BoxDecoration(shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.white, blurRadius: 10 * sf, spreadRadius: 3 * sf)]) : null, + child: _circle(label, color, icon, false, baseSize, feedSize, sf, isX: isX) + ), ); } ), ); } - Widget _actionBtn(String label, Color color, VoidCallback onTap, {IconData? icon, bool isX = false, double labelSize = 24}) { - return GestureDetector(onTap: onTap, child: _circle(label, color, icon, false, fontSize: labelSize, isX: isX)); - } - - Widget _circle(String label, Color color, IconData? icon, bool isFeed, {double fontSize = 20, bool isX = false}) { + Widget _circle(String label, Color color, IconData? icon, bool isFeed, double baseSize, double feedSize, double sf, {bool isX = false}) { + double size = isFeed ? feedSize : baseSize; Widget content; - bool isPointBtn = label == "1" || label == "2" || label == "3" || label == "M2" || label == "M3"; + bool isPointBtn = label == "1" || label == "2" || label == "3" || label == "M1" || label == "M2" || label == "M3"; bool isBlkBtn = label == "BLK"; if (isPointBtn) { content = Stack( alignment: Alignment.center, children: [ - Container(width: isFeed ? 55 : 45, height: isFeed ? 55 : 45, decoration: const BoxDecoration(color: Colors.black, shape: BoxShape.circle)), - Icon(Icons.sports_basketball, color: color, size: isFeed ? 65 : 55), + Container(width: size * 0.75, height: size * 0.75, decoration: const BoxDecoration(color: Colors.black, shape: BoxShape.circle)), + Icon(Icons.sports_basketball, color: color, size: size * 0.9), Stack( children: [ - Text(label, style: TextStyle(fontSize: isFeed ? 26 : 22, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = 3..color = Colors.white, decoration: TextDecoration.none)), - Text(label, style: TextStyle(fontSize: isFeed ? 26 : 22, fontWeight: FontWeight.w900, color: Colors.black, decoration: TextDecoration.none)), + Text(label, style: TextStyle(fontSize: size * 0.38, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = size * 0.05..color = Colors.white, decoration: TextDecoration.none)), + Text(label, style: TextStyle(fontSize: size * 0.38, fontWeight: FontWeight.w900, color: Colors.black, decoration: TextDecoration.none)), ], ), ], ); - } - else if (isBlkBtn) { + } else if (isBlkBtn) { content = Stack( alignment: Alignment.center, children: [ - Icon(Icons.front_hand, color: const Color.fromARGB(207, 56, 52, 52), size: isFeed ? 55 : 45), + Icon(Icons.front_hand, color: const Color.fromARGB(207, 56, 52, 52), size: size * 0.75), Stack( alignment: Alignment.center, children: [ - Text(label, style: TextStyle(fontSize: isFeed ? 18 : 16, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = 3..color = Colors.black, decoration: TextDecoration.none)), - Text(label, style: TextStyle(fontSize: isFeed ? 18 : 16, fontWeight: FontWeight.w900, color: Colors.white, decoration: TextDecoration.none)), + Text(label, style: TextStyle(fontSize: size * 0.28, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = size * 0.05..color = Colors.black, decoration: TextDecoration.none)), + Text(label, style: TextStyle(fontSize: size * 0.28, fontWeight: FontWeight.w900, color: Colors.white, decoration: TextDecoration.none)), ], ), ], ); } else if (icon != null) { - content = Icon(icon, color: Colors.white, size: 30); + content = Icon(icon, color: Colors.white, size: size * 0.5); } else { - content = Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: fontSize, decoration: TextDecoration.none)); + content = Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: size * 0.35, decoration: TextDecoration.none)); } return Stack( - clipBehavior: Clip.none, alignment: Alignment.bottomRight, + clipBehavior: Clip.none, + alignment: Alignment.bottomRight, children: [ Container( width: size, height: size, - decoration: (isPointBtn || isBlkBtn) ? const BoxDecoration(color: Colors.transparent) : BoxDecoration(gradient: RadialGradient(colors: [color.withOpacity(0.7), color], radius: 0.8), shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black38, blurRadius: 6 * sf, offset: Offset(0, 3 * sf))]), + decoration: (isPointBtn || isBlkBtn) + ? const BoxDecoration(color: Colors.transparent) + : BoxDecoration(gradient: RadialGradient(colors: [color.withOpacity(0.7), color], radius: 0.8), shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black38, blurRadius: 6 * sf, offset: Offset(0, 3 * sf))]), alignment: Alignment.center, child: content, ), - if (isX) Positioned(top: 0, right: 0, child: Container(decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), child: Icon(Icons.cancel, color: Colors.red, size: isFeed ? 28 : 24))), + if (isX) Positioned(top: 0, right: 0, child: Container(decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), child: Icon(Icons.cancel, color: Colors.red, size: size * 0.4))), ], ); } diff --git a/lib/widgets/team_widgets.dart b/lib/widgets/team_widgets.dart index 2b09247..3075c66 100644 --- a/lib/widgets/team_widgets.dart +++ b/lib/widgets/team_widgets.dart @@ -7,154 +7,61 @@ import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER! // --- CABEÇALHO --- class StatsHeader extends StatelessWidget { final Team team; - final TeamController controller; - final VoidCallback onFavoriteTap; - final double sf; // <-- Variável de escala - const TeamCard({ - super.key, - required this.team, - required this.controller, - required this.onFavoriteTap, - required this.sf, - }); + const StatsHeader({super.key, required this.team}); @override Widget build(BuildContext context) { - return Card( - color: Colors.white, - elevation: 3, - margin: EdgeInsets.only(bottom: 12 * sf), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)), - child: ListTile( - contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf), - - // --- 1. IMAGEM + FAVORITO --- - leading: Stack( - clipBehavior: Clip.none, - children: [ - 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( - 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), - ), - ], + return Container( + padding: EdgeInsets.only( + top: 50 * context.sf, + left: 20 * context.sf, + right: 20 * context.sf, + bottom: 20 * context.sf + ), + decoration: BoxDecoration( + color: AppTheme.primaryRed, // 👇 Usando a cor do teu tema! + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(30 * context.sf), + bottomRight: Radius.circular(30 * context.sf) ), ), - ); - } - - void _confirmDelete(BuildContext context) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold)), - content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * sf)), - actions: [ - TextButton( + child: Row( + children: [ + IconButton( + icon: Icon(Icons.arrow_back, color: Colors.white, size: 24 * context.sf), onPressed: () => Navigator.pop(context), - child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf)), ), - TextButton( - onPressed: () { - controller.deleteTeam(team.id); - Navigator.pop(context); - }, - child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * sf)), + SizedBox(width: 10 * context.sf), + CircleAvatar( + radius: 24 * context.sf, + backgroundColor: Colors.white24, + 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: 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) + ), + ], + ), ), ], ), @@ -162,90 +69,164 @@ class StatsHeader extends StatelessWidget { } } -// --- DIALOG DE CRIAÇÃO --- -class CreateTeamDialog extends StatefulWidget { - final Function(String name, String season, String imageUrl) onConfirm; - final double sf; // Recebe a escala +// --- CARD DE RESUMO --- +class StatsSummaryCard extends StatelessWidget { + final int total; - const CreateTeamDialog({super.key, required this.onConfirm, required this.sf}); - - @override - State createState() => _CreateTeamDialogState(); -} - -class _CreateTeamDialogState extends State { - final TextEditingController _nameController = TextEditingController(); - final TextEditingController _imageController = TextEditingController(); - String _selectedSeason = '2024/25'; + const StatsSummaryCard({super.key, required this.total}); @override Widget build(BuildContext context) { - return AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)), - title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold)), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, + // 👇 Adaptável ao Modo Escuro + final cardColor = Theme.of(context).brightness == Brightness.dark + ? const Color(0xFF1E1E1E) + : Colors.white; + + 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: [ - TextField( - controller: _nameController, - style: TextStyle(fontSize: 14 * widget.sf), - decoration: InputDecoration( - labelText: 'Nome da Equipa', - labelStyle: TextStyle(fontSize: 14 * widget.sf) - ), - textCapitalization: TextCapitalization.words, + Row( + 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, // 👇 Adaptável + fontSize: 16 * context.sf, + fontWeight: FontWeight.w600 + ) + ), + ], ), - SizedBox(height: 15 * widget.sf), - DropdownButtonFormField( - value: _selectedSeason, - decoration: InputDecoration( - labelText: 'Temporada', - labelStyle: TextStyle(fontSize: 14 * widget.sf) - ), - 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) - ), + Text( + "$total", + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável + fontSize: 28 * context.sf, + fontWeight: FontWeight.bold + ) ), ], ), ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf)) - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFE74C3C), - 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); - } - }, - child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)), + ); + } +} + +// --- TÍTULO DE SECÇÃO --- +class StatsSectionTitle extends StatelessWidget { + final String title; + + const StatsSectionTitle({super.key, required this.title}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 18 * context.sf, + fontWeight: FontWeight.bold, + 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, + ), + ], + ), + ), + ); + } } \ No newline at end of file