diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a2ed977..c1984fb 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,7 +9,7 @@ android { namespace = "com.example.playmaker" compileSdk = flutter.compileSdkVersion //ndkVersion = flutter.ndkVersion -ndkVersion = "27.0.12077973" + ndkVersion = "26.1.10909125" compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 diff --git a/assets/playmaker-logos.png b/assets/playmaker-logos.png deleted file mode 100644 index 95e69a1..0000000 Binary files a/assets/playmaker-logos.png and /dev/null differ diff --git a/lib/calibrador_page.dart b/lib/calibrador_page.dart deleted file mode 100644 index f3ed3b1..0000000 --- a/lib/calibrador_page.dart +++ /dev/null @@ -1,208 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'dart:math' as math; - -class CalibradorPage extends StatefulWidget { - const CalibradorPage({super.key}); - - @override - State createState() => _CalibradorPageState(); -} - -class _CalibradorPageState extends State { - // --- 👇 VALORES INICIAIS 👇 --- - double hoopBaseX = 0.08; - double arcRadius = 0.28; - double cornerY = 0.40; - // ----------------------------------------------------- - - @override - void initState() { - super.initState(); - SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeRight, - DeviceOrientation.landscapeLeft, - ]); - } - - @override - void dispose() { - SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final double wScreen = MediaQuery.of(context).size.width; - final double hScreen = MediaQuery.of(context).size.height; - - // O MESMO CÁLCULO EXATO DO PLACAR - final double sf = math.min(wScreen / 1150, hScreen / 720); - - return Scaffold( - backgroundColor: const Color(0xFF266174), - body: SafeArea( - top: false, - bottom: false, - child: Stack( - children: [ - // 👇 1. O CAMPO COM AS MARGENS EXATAS DO PLACAR 👇 - Container( - margin: EdgeInsets.only(left: 65 * sf, right: 65 * sf, bottom: 55 * sf), - decoration: BoxDecoration( - border: Border.all(color: Colors.white, width: 2.5), - image: const DecorationImage( - image: AssetImage('assets/campo.png'), - fit: BoxFit.fill, - ), - ), - child: LayoutBuilder( - builder: (context, constraints) { - return CustomPaint( - painter: LinePainter( - hoopBaseX: hoopBaseX, - arcRadius: arcRadius, - cornerY: cornerY, - color: Colors.redAccent, - width: constraints.maxWidth, - height: constraints.maxHeight, - ), - ); - }, - ), - ), - - // 👇 2. TOPO: MOSTRADORES DE VALORES COM FITTEDBOX (Não transborda) 👇 - Positioned( - top: 0, left: 0, right: 0, - child: Container( - color: Colors.black87.withOpacity(0.8), - padding: EdgeInsets.symmetric(vertical: 5 * sf, horizontal: 15 * sf), - child: FittedBox( // Isto impede o ecrã de dar o erro dos 179 pixels! - fit: BoxFit.scaleDown, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildValueDisplay("Aro X", hoopBaseX, sf), - SizedBox(width: 20 * sf), - _buildValueDisplay("Raio", arcRadius, sf), - SizedBox(width: 20 * sf), - _buildValueDisplay("Canto", cornerY, sf), - SizedBox(width: 30 * sf), - ElevatedButton.icon( - onPressed: () => Navigator.pop(context), - icon: Icon(Icons.check, size: 18 * sf), - label: Text("FECHAR", style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold)), - style: ElevatedButton.styleFrom(backgroundColor: Colors.green), - ) - ], - ), - ), - ), - ), - - // 👇 3. FUNDO: SLIDERS (Com altura fixa para não dar o erro "hasSize") 👇 - Positioned( - bottom: 0, left: 0, right: 0, - child: Container( - color: Colors.black87.withOpacity(0.8), - height: 80 * sf, // Altura segura para os sliders - child: Row( - children: [ - Expanded(child: _buildSlider("Pos. do Aro", hoopBaseX, 0.0, 0.25, (val) => setState(() => hoopBaseX = val), sf)), - Expanded(child: _buildSlider("Tam. da Curva", arcRadius, 0.1, 0.5, (val) => setState(() => arcRadius = val), sf)), - Expanded(child: _buildSlider("Pos. do Canto", cornerY, 0.2, 0.5, (val) => setState(() => cornerY = val), sf)), - ], - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildValueDisplay(String label, double value, double sf) { - return Row( - children: [ - Text("$label: ", style: TextStyle(color: Colors.white70, fontSize: 16 * sf)), - Text(value.toStringAsFixed(3), style: TextStyle(color: Colors.yellow, fontSize: 20 * sf, fontWeight: FontWeight.bold)), - ], - ); - } - - Widget _buildSlider(String label, double value, double min, double max, ValueChanged onChanged, double sf) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(label, style: TextStyle(color: Colors.white, fontSize: 12 * sf)), - SizedBox( - height: 40 * sf, // Altura exata para o Slider não crashar - child: Slider( - value: value, min: min, max: max, - activeColor: Colors.yellow, inactiveColor: Colors.white24, - onChanged: onChanged, - ), - ), - ], - ); - } -} - -// ============================================================== -// 📐 PINTOR: DESENHA A LINHA MATEMÁTICA NA TELA -// ============================================================== -class LinePainter extends CustomPainter { - final double hoopBaseX; - final double arcRadius; - final double cornerY; - final Color color; - final double width; - final double height; - - LinePainter({ - required this.hoopBaseX, required this.arcRadius, required this.cornerY, - required this.color, required this.width, required this.height, - }); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..style = PaintingStyle.stroke - ..strokeWidth = 4; - - double aspectRatio = width / height; - double hoopY = 0.50 * height; - - // O cornerY controla a que distância do meio (50%) estão as linhas retas - double cornerDistY = cornerY * height; - - // --- CESTO ESQUERDO --- - double hoopLX = hoopBaseX * width; - - canvas.drawLine(Offset(0, hoopY - cornerDistY), Offset(width * 0.35, hoopY - cornerDistY), paint); // Cima - canvas.drawLine(Offset(0, hoopY + cornerDistY), Offset(width * 0.35, hoopY + cornerDistY), paint); // Baixo - - canvas.drawArc( - Rect.fromCenter(center: Offset(hoopLX, hoopY), width: arcRadius * width * 2 / aspectRatio, height: arcRadius * height * 2), - -math.pi / 2, math.pi, false, paint, - ); - - // --- CESTO DIREITO --- - double hoopRX = (1.0 - hoopBaseX) * width; - - canvas.drawLine(Offset(width, hoopY - cornerDistY), Offset(width * 0.65, hoopY - cornerDistY), paint); // Cima - canvas.drawLine(Offset(width, hoopY + cornerDistY), Offset(width * 0.65, hoopY + cornerDistY), paint); // Baixo - - canvas.drawArc( - Rect.fromCenter(center: Offset(hoopRX, hoopY), width: arcRadius * width * 2 / aspectRatio, height: arcRadius * height * 2), - math.pi / 2, math.pi, false, paint, - ); - } - - @override - bool shouldRepaint(covariant LinePainter oldDelegate) { - return oldDelegate.hoopBaseX != hoopBaseX || oldDelegate.arcRadius != arcRadius || oldDelegate.cornerY != cornerY; - } -} \ No newline at end of file diff --git a/lib/controllers/game_controller.dart b/lib/controllers/game_controller.dart index b55decf..ffa141a 100644 --- a/lib/controllers/game_controller.dart +++ b/lib/controllers/game_controller.dart @@ -4,34 +4,25 @@ import '../models/game_model.dart'; class GameController { final _supabase = Supabase.instance.client; - // 1. LER JOGOS (Com Filtros Opcionais) - Stream> getFilteredGames({String? teamFilter, String? seasonFilter}) { + // 1. LER JOGOS (Stream em Tempo Real) +Stream> get gamesStream { return _supabase - .from('games') + .from('games') // 1. Fica à escuta da tabela original (Garante o Tempo Real!) .stream(primaryKey: ['id']) .asyncMap((event) async { - - // 👇 A CORREÇÃO ESTÁ AQUI: Lê diretamente da tabela 'games' - var query = _supabase.from('games').select(); - - // Aplica o filtro de Temporada - if (seasonFilter != null && seasonFilter.isNotEmpty && seasonFilter != 'Todas') { - query = query.eq('season', seasonFilter); - } - - // Aplica o filtro de Equipa (Procura em casa ou fora) - if (teamFilter != null && teamFilter.isNotEmpty && teamFilter != 'Todas') { - query = query.or('my_team.eq.$teamFilter,opponent_team.eq.$teamFilter'); - } - - // Executa a query com a ordenação por data - final viewData = await query.order('game_date', ascending: false); + // 2. Sempre que a tabela 'games' mudar (novo jogo, alteração de resultado), + // vamos buscar os dados já misturados com as imagens à nossa View. + final viewData = await _supabase + .from('games_with_logos') + .select() + .order('game_date', ascending: false); + // 3. Convertemos para a nossa lista de objetos Game return viewData.map((json) => Game.fromMap(json)).toList(); }); } - // 2. CRIAR JOGO + // Retorna o ID do jogo criado para podermos navegar para o placar Future createGame(String myTeam, String opponent, String season) async { try { final response = await _supabase.from('games').insert({ @@ -40,16 +31,18 @@ class GameController { 'season': season, 'my_score': 0, 'opponent_score': 0, - 'status': 'Decorrer', + 'status': 'Decorrer', // Começa como "Decorrer" 'game_date': DateTime.now().toIso8601String(), - }).select().single(); + }).select().single(); // .select().single() retorna o objeto criado - return response['id']; + return response['id']; // Retorna o UUID gerado pelo Supabase } catch (e) { print("Erro ao criar jogo: $e"); return null; } } - void dispose() {} + void dispose() { + // Não é necessário fechar streams do Supabase manualmente aqui + } } \ No newline at end of file diff --git a/lib/controllers/stats_controller.dart b/lib/controllers/stats_controller.dart new file mode 100644 index 0000000..aa3a955 --- /dev/null +++ b/lib/controllers/stats_controller.dart @@ -0,0 +1,158 @@ +/*import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import '../models/person_model.dart'; + +class StatsController { + final SupabaseClient _supabase = Supabase.instance.client; + + // 1. LER + Stream> getMembers(String teamId) { + return _supabase + .from('members') + .stream(primaryKey: ['id']) + .eq('team_id', teamId) + .order('name', ascending: true) + .map((data) => data.map((json) => Person.fromMap(json)).toList()); + } + + // 2. APAGAR + Future deletePerson(String personId) async { + try { + await _supabase.from('members').delete().eq('id', personId); + } catch (e) { + debugPrint("Erro ao eliminar: $e"); + } + } + + // 3. DIÁLOGOS + void showAddPersonDialog(BuildContext context, String teamId) { + _showForm(context, teamId: teamId); + } + + void showEditPersonDialog(BuildContext context, String teamId, Person person) { + _showForm(context, teamId: teamId, person: person); + } + + // --- O POPUP ESTÁ AQUI --- + void _showForm(BuildContext context, {required String teamId, Person? person}) { + final isEdit = person != null; + final nameCtrl = TextEditingController(text: person?.name ?? ''); + final numCtrl = TextEditingController(text: person?.number ?? ''); + + // Define o valor inicial + String selectedType = person?.type ?? 'Jogador'; + + showDialog( + context: context, + builder: (ctx) => StatefulBuilder( + builder: (ctx, setState) => AlertDialog( + title: Text(isEdit ? "Editar" : "Adicionar"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // NOME + TextField( + controller: nameCtrl, + decoration: const InputDecoration(labelText: "Nome"), + textCapitalization: TextCapitalization.sentences, + ), + const SizedBox(height: 10), + + // FUNÇÃO + DropdownButtonFormField( + value: selectedType, + decoration: const InputDecoration(labelText: "Função"), + items: ["Jogador", "Treinador"] + .map((e) => DropdownMenuItem(value: e, child: Text(e))) + .toList(), + onChanged: (v) { + if (v != null) setState(() => selectedType = v); + }, + ), + + // NÚMERO (Só aparece se for Jogador) + if (selectedType == "Jogador") ...[ + const SizedBox(height: 10), + TextField( + controller: numCtrl, + decoration: const InputDecoration(labelText: "Número da Camisola"), + keyboardType: TextInputType.text, // Aceita texto para evitar erros + ), + ], + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text("Cancelar") + ), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF00C853)), + onPressed: () async { + print("--- 1. CLICOU EM GUARDAR ---"); + + // Validação Simples + if (nameCtrl.text.trim().isEmpty) { + print("ERRO: Nome vazio"); + return; + } + + // Lógica do Número: + // Se for Treinador -> envia NULL + // Se for Jogador e estiver vazio -> envia NULL + // Se tiver texto -> envia o Texto + String? numeroFinal; + if (selectedType == "Treinador") { + numeroFinal = null; + } else { + numeroFinal = numCtrl.text.trim().isEmpty ? null : numCtrl.text.trim(); + } + + print("--- 2. DADOS A ENVIAR ---"); + print("Nome: ${nameCtrl.text}"); + print("Tipo: $selectedType"); + print("Número: $numeroFinal"); + + try { + if (isEdit) { + await _supabase.from('members').update({ + 'name': nameCtrl.text.trim(), + 'type': selectedType, + 'number': numeroFinal, + }).eq('id', person!.id); + } else { + await _supabase.from('members').insert({ + 'team_id': teamId, // Verifica se este teamId é válido! + 'name': nameCtrl.text.trim(), + 'type': selectedType, + 'number': numeroFinal, + }); + } + + print("--- 3. SUCESSO! FECHANDO DIÁLOGO ---"); + if (ctx.mounted) Navigator.pop(ctx); + + } catch (e) { + print("--- X. ERRO AO GUARDAR ---"); + print(e.toString()); + + // MOSTRA O ERRO NO TELEMÓVEL + if (ctx.mounted) { + ScaffoldMessenger.of(ctx).showSnackBar( + SnackBar( + content: Text("Erro: $e"), + backgroundColor: Colors.red, + duration: const Duration(seconds: 4), + ), + ); + } + } + }, + child: const Text("Guardar", style: TextStyle(color: Colors.white)), + ) + ], + ), + ), + ); + } +}*/ \ No newline at end of file diff --git a/lib/controllers/team_controller.dart b/lib/controllers/team_controller.dart index d0b3dd6..bf45320 100644 --- a/lib/controllers/team_controller.dart +++ b/lib/controllers/team_controller.dart @@ -1,24 +1,21 @@ import 'package:supabase_flutter/supabase_flutter.dart'; class TeamController { + // Instância do cliente Supabase final _supabase = Supabase.instance.client; - // 1. Variável fixa para guardar o Stream principal - late final Stream>> teamsStream; - - // 2. Dicionário (Cache) para não recriar Streams de contagem repetidos - final Map> _playerCountStreams = {}; - - TeamController() { - // INICIALIZAÇÃO: O stream é criado APENAS UMA VEZ quando abres a página! - teamsStream = _supabase + // 1. STREAM (Realtime) + // Adicionei o .map() no final para garantir que o Dart entende que é uma List + Stream>> get teamsStream { + return _supabase .from('teams') .stream(primaryKey: ['id']) .order('name', ascending: true) .map((data) => List>.from(data)); } - // CRIAR + // 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({ @@ -33,50 +30,42 @@ class TeamController { } } - // ELIMINAR + // 3. ELIMINAR Future deleteTeam(String id) async { try { await _supabase.from('teams').delete().eq('id', id); - // Limpa o cache deste teamId se a equipa for apagada - _playerCountStreams.remove(id); } catch (e) { print("❌ Erro ao eliminar: $e"); } } - // FAVORITAR + // 4. FAVORITAR Future toggleFavorite(String teamId, bool currentStatus) async { try { await _supabase .from('teams') - .update({'is_favorite': !currentStatus}) + .update({'is_favorite': !currentStatus}) // Inverte o valor .eq('id', teamId); } catch (e) { print("❌ Erro ao favoritar: $e"); } } - // CONTAR JOGADORES (AGORA COM CACHE DE MEMÓRIA!) - Stream getPlayerCountStream(String teamId) { - // Se já criámos um "Tubo de ligação" para esta equipa, REUTILIZA-O! - if (_playerCountStreams.containsKey(teamId)) { - return _playerCountStreams[teamId]!; + // 5. CONTAR JOGADORES + // CORRIGIDO: A sintaxe antiga dava erro. O método .count() é o correto agora. + Future getPlayerCount(String teamId) async { + try { + final count = await _supabase + .from('members') + .count() // Retorna diretamente o número inteiro + .eq('team_id', teamId); + return count; + } catch (e) { + print("Erro ao contar jogadores: $e"); + return 0; } - - // Se é a primeira vez que pede esta equipa, cria a ligação e guarda na memória - final newStream = _supabase - .from('members') - .stream(primaryKey: ['id']) - .eq('team_id', teamId) - .map((data) => data.length); - - _playerCountStreams[teamId] = newStream; // Guarda no dicionário - return newStream; } - // LIMPEZA FINAL QUANDO SAÍMOS DA PÁGINA - void dispose() { - // Limpamos o dicionário de streams para libertar memória RAM - _playerCountStreams.clear(); - } + // Mantemos o dispose vazio para não quebrar a chamada na TeamsPage + void dispose() {} } \ 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 7aae400..fa11e46 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'; +import '../dados_grafico.dart'; // Ajusta o caminho se der erro de import 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, + draws: draws ?? _chartData.draws, // 👇 AGORA ELE ACEITA OS EMPATES ); notifyListeners(); } diff --git a/lib/grafico de pizza/dados_grafico.dart b/lib/grafico de pizza/dados_grafico.dart index 8b559f0..17b131f 100644 --- a/lib/grafico de pizza/dados_grafico.dart +++ b/lib/grafico de pizza/dados_grafico.dart @@ -22,6 +22,5 @@ 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 95b5a6c..eb7a40e 100644 --- a/lib/grafico de pizza/grafico.dart +++ b/lib/grafico de pizza/grafico.dart @@ -1,28 +1,21 @@ 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 int victories; - final int defeats; - final int draws; + final PieChartController? controller; final String title; final String subtitle; final Color? backgroundColor; final VoidCallback? onTap; - final double sf; const PieChartCard({ super.key, - this.victories = 0, - this.defeats = 0, - this.draws = 0, + this.controller, this.title = 'DESEMPENHO', this.subtitle = 'Temporada', this.onTap, - this.backgroundColor, + required this.backgroundColor, this.sf = 1.0, }); @@ -31,26 +24,30 @@ class PieChartCard extends StatefulWidget { } class _PieChartCardState extends State with SingleTickerProviderStateMixin { + late PieChartController _controller; late AnimationController _animationController; late Animation _animation; @override void initState() { super.initState(); - _animationController = AnimationController(duration: const Duration(milliseconds: 600), vsync: this); - _animation = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeOutBack)); + _controller = widget.controller ?? PieChartController(); + + _animationController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + + _animation = Tween(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(); @@ -61,31 +58,30 @@ class _PieChartCardState extends State with SingleTickerProviderSt Widget build(BuildContext context) { final data = PieChartData(victories: widget.victories, defeats: widget.defeats, draws: widget.draws); - // 👇 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( + 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(opacity: _animation.value.clamp(0.0, 1.0), child: child), + child: Opacity( + // 👇 AQUI ESTÁ A FIX: Garante que fica entre 0 e 1 + opacity: _animation.value.clamp(0.0, 1.0), + child: child, + ), ); }, child: Card( margin: EdgeInsets.zero, - elevation: 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 - ), + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), child: InkWell( onTap: widget.onTap, + borderRadius: BorderRadius.circular(14), child: Container( decoration: BoxDecoration( - color: cardColor, // 👇 APLICA A COR BLINDADA + borderRadius: BorderRadius.circular(14), + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [widget.backgroundColor.withOpacity(0.9), widget.backgroundColor.withOpacity(0.7)]), ), child: LayoutBuilder( builder: (context, constraints) { @@ -93,147 +89,161 @@ class _PieChartCardState extends State with SingleTickerProviderSt final double cw = constraints.maxWidth; return Padding( - padding: EdgeInsets.symmetric(horizontal: cw * 0.05, vertical: ch * 0.03), + padding: EdgeInsets.all(cw * 0.06), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // --- CABEÇALHO --- (👇 MANTIDO ALINHADO À ESQUERDA) + // 👇 TÍTULOS UM POUCO MAIS PRESENTES FittedBox( fit: BoxFit.scaleDown, - child: Text(widget.title.toUpperCase(), - style: TextStyle( - fontSize: ch * 0.045, - fontWeight: FontWeight.bold, - color: AppTheme.primaryRed, // 👇 USANDO O TEU primaryRed - letterSpacing: 1.2 - ) - ), + child: Text(widget.title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), letterSpacing: 1.0)), ), - Text(widget.subtitle, - style: TextStyle( - fontSize: ch * 0.055, - fontWeight: FontWeight.bold, - color: AppTheme.backgroundLight, // 👇 USANDO O TEU backgroundLight - ) + FittedBox( + fit: BoxFit.scaleDown, + child: Text(widget.subtitle, style: TextStyle(fontSize: ch * 0.07, fontWeight: FontWeight.bold, color: Colors.white)), ), - const Expanded(flex: 1, child: SizedBox()), + SizedBox(height: ch * 0.03), - // --- MIOLO (GRÁFICO MAIOR À ESQUERDA + STATS) --- - Expanded( - flex: 9, + // MEIO (GRÁFICO + ESTATÍSTICAS) + Expanded( child: Row( - mainAxisAlignment: MainAxisAlignment.end, // Changed from spaceBetween to end to push stats more to the right + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 1. Lado Esquerdo: Donut Chart - // 👇 MUDANÇA AQUI: Gráfico ainda maior! cw * 0.52 - SizedBox( - width: cw * 0.52, - height: cw * 0.52, + Expanded( + flex: 1, child: PieChartWidget( victoryPercentage: data.victoryPercentage, defeatPercentage: data.defeatPercentage, drawPercentage: data.drawPercentage, - sf: widget.sf, + sf: widget.sf, ), ), - - SizedBox(width: cw * 0.005), // Reduzi o espaço no meio para dar lugar ao gráfico - - // 2. Lado Direito: Números Dinâmicos + SizedBox(width: cw * 0.05), Expanded( - 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), - ], - ), + 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), + ], ), ), ], ), ), - - const Expanded(flex: 1, child: SizedBox()), - - // --- RODAPÉ: BOTÃO WIN RATE GIGANTE --- (👇 MUDANÇA AQUI: Alinhado à esquerda) + + // 👇 RODAPÉ AJUSTADO + SizedBox(height: ch * 0.03), Container( width: double.infinity, - padding: EdgeInsets.symmetric(vertical: ch * 0.025), + padding: EdgeInsets.symmetric(vertical: ch * 0.035), decoration: BoxDecoration( - color: textColor.withOpacity(0.05), // 👇 Fundo adaptável - borderRadius: BorderRadius.circular(12), + color: Colors.white24, // Igual ao fundo do botão detalhes + borderRadius: BorderRadius.circular(ch * 0.03), // Borda arredondada ), - 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 + 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 + ) ), ), ], ), - ), + ], ), - ], - ), - ); - } + ), + + 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, + ), + ), + ], + ), + ), + ], + ), + ), ), ), ), ), ); } - - // 👇 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) { + // 👇 PERCENTAGENS SUBIDAS LIGEIRAMENTE (0.10 e 0.045) + Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch) { return Padding( - padding: EdgeInsets.symmetric(vertical: ch * 0.005), + padding: EdgeInsets.only(bottom: ch * 0.01), child: Row( - mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox( - width: cw * 0.12, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Text(label, style: TextStyle(fontSize: ch * 0.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) - ], - ), + // Número subiu para 0.10 + Expanded(flex: 2, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(number, style: TextStyle(fontSize: ch * 0.10, fontWeight: FontWeight.bold, color: color, height: 1.0)))), + SizedBox(width: ch * 0.02), + Expanded( + flex: 3, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ + Row(children: [ + Container(width: ch * 0.018, height: ch * 0.018, margin: EdgeInsets.only(right: ch * 0.015), decoration: BoxDecoration(color: color, shape: BoxShape.circle)), + // Label subiu para 0.045 + Expanded(child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(label, style: TextStyle(fontSize: ch * 0.033, color: Colors.white.withOpacity(0.8), fontWeight: FontWeight.w600)))) + ]), + // Percentagem subiu para 0.05 + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('$percent%', style: TextStyle(fontSize: ch * 0.04, color: color, fontWeight: FontWeight.bold))), + ]), ), - SizedBox(width: cw * 0.03), - Text(number, style: TextStyle(fontSize: ch * 0.15, fontWeight: FontWeight.w900, color: statColor, height: 1)), // (increased from 0.125) ], ), ); } - 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) - ); + Widget _buildDynDivider(double ch) { + return Container(height: 0.5, color: Colors.white.withOpacity(0.1), margin: EdgeInsets.symmetric(vertical: ch * 0.01)); } } \ 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 d7ac90f..1a56ed2 100644 --- a/lib/grafico de pizza/widgets/grafico_widgets.dart +++ b/lib/grafico de pizza/widgets/grafico_widgets.dart @@ -5,23 +5,26 @@ class PieChartWidget extends StatelessWidget { final double victoryPercentage; final double defeatPercentage; final double drawPercentage; - final double sf; + final double size; const PieChartWidget({ super.key, required this.victoryPercentage, required this.defeatPercentage, this.drawPercentage = 0, - required this.sf, + this.size = 140, // Aumentado para 400x300 }); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { + // 👇 MAGIA ANTI-DESAPARECIMENTO 👇 + // Vê o espaço real. Se por algum motivo for infinito, assume 100 para não sumir. final double w = constraints.maxWidth.isInfinite ? 100.0 : constraints.maxWidth; final double h = constraints.maxHeight.isInfinite ? 100.0 : constraints.maxHeight; + // Pega no menor valor para garantir que o círculo não é cortado final double size = math.min(w, h); return Center( @@ -29,7 +32,7 @@ class PieChartWidget extends StatelessWidget { width: size, height: size, child: CustomPaint( - painter: _DonutChartPainter( + painter: _PieChartPainter( victoryPercentage: victoryPercentage, defeatPercentage: defeatPercentage, drawPercentage: drawPercentage, @@ -45,27 +48,24 @@ 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( - // 👇 Casa decimal aplicada aqui! - hasGames ? '${(victoryPercentage * 100).toStringAsFixed(1)}%' : '---', + '${(victoryPercentage * 100).toStringAsFixed(1)}%', style: TextStyle( - fontSize: size * (hasGames ? 0.20 : 0.15), + fontSize: size * 0.18, // O texto cresce ou encolhe com o círculo fontWeight: FontWeight.bold, - color: hasGames ? Colors.white : Colors.white54, + color: Colors.white, ), ), SizedBox(height: size * 0.02), Text( - hasGames ? 'Vitórias' : 'Sem Jogos', + 'Vitórias', style: TextStyle( - fontSize: size * 0.08, - color: hasGames ? Colors.white70 : Colors.white38, + fontSize: size * 0.10, + color: Colors.white.withOpacity(0.8), ), ), ], @@ -87,40 +87,59 @@ class _DonutChartPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); - final radius = (size.width / 2) - (size.width * 0.1); - final strokeWidth = size.width * 0.2; + // Margem de 5% para a linha de fora não ser cortada + final radius = (size.width / 2) - (size.width * 0.05); - 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.amber; - - double startAngle = -math.pi / 2; + const drawColor = Colors.yellow; + const borderColor = Colors.white30; - 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 = strokeWidth - ..strokeCap = StrokeCap.butt; - - canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, paint); + 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; } - drawDonutSector(victoryPercentage, victoryColor); - drawDonutSector(drawPercentage, drawColor); - drawDonutSector(defeatPercentage, defeatColor); + 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) + ..style = PaintingStyle.stroke + ..strokeWidth = totalWidth * 0.015; + + final lineX = center.dx + radius * math.cos(startAngle); + final lineY = center.dy + radius * math.sin(startAngle); + + canvas.drawLine(center, Offset(lineX, lineY), linePaint); + } } @override diff --git a/lib/models/game_model.dart b/lib/models/game_model.dart index 763c33e..64f2f79 100644 --- a/lib/models/game_model.dart +++ b/lib/models/game_model.dart @@ -2,6 +2,8 @@ class Game { final String id; final String myTeam; final String opponentTeam; + final String? myTeamLogo; // URL da imagem + final String? opponentTeamLogo; // URL da imagem final String myScore; final String opponentScore; final String status; @@ -11,22 +13,26 @@ class Game { required this.id, required this.myTeam, required this.opponentTeam, + this.myTeamLogo, + this.opponentTeamLogo, required this.myScore, required this.opponentScore, required this.status, required this.season, }); + // No seu factory, certifique-se de mapear os campos da tabela (ou de um JOIN) factory Game.fromMap(Map map) { return Game( - // O "?." converte para texto com segurança, e o "?? '...'" diz o que mostrar se for nulo (vazio) - id: map['id']?.toString() ?? '', - myTeam: map['my_team']?.toString() ?? 'Desconhecida', - opponentTeam: map['opponent_team']?.toString() ?? 'Adversário', - myScore: map['my_score']?.toString() ?? '0', - opponentScore: map['opponent_score']?.toString() ?? '0', - status: map['status']?.toString() ?? 'Terminado', - season: map['season']?.toString() ?? 'Sem Época', + id: map['id'], + myTeam: map['my_team_name'], + opponentTeam: map['opponent_team_name'], + myTeamLogo: map['my_team_logo'], // Certifique-se que o Supabase retorna isto + opponentTeamLogo: map['opponent_team_logo'], + myScore: map['my_score'].toString(), + opponentScore: map['opponent_score'].toString(), + status: map['status'], + season: map['season'], ); } } \ No newline at end of file diff --git a/lib/pages/gamePage.dart b/lib/pages/gamePage.dart index fcd733d..a1f8113 100644 --- a/lib/pages/gamePage.dart +++ b/lib/pages/gamePage.dart @@ -1,91 +1,77 @@ import 'package:flutter/material.dart'; import 'package:playmaker/pages/PlacarPage.dart'; -import 'package:playmaker/classe/theme.dart'; +import '../controllers/game_controller.dart'; import '../controllers/team_controller.dart'; import '../controllers/game_controller.dart'; import '../models/game_model.dart'; -import '../utils/size_extension.dart'; +import '../utils/size_extension.dart'; // 👇 NOVO SUPERPODER AQUI TAMBÉM! // --- CARD DE EXIBIÇÃO DO JOGO --- class GameResultCard extends StatelessWidget { final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season; - final String? myTeamLogo, opponentTeamLogo; - final double sf; + final String? myTeamLogo, opponentTeamLogo; const GameResultCard({ super.key, required this.gameId, required this.myTeam, required this.opponentTeam, required this.myScore, required this.opponentScore, required this.status, required this.season, - this.myTeamLogo, this.opponentTeamLogo, required this.sf, + this.myTeamLogo, this.opponentTeamLogo, }); @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 * 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)), - ), + margin: EdgeInsets.only(bottom: 16 * context.sf), + padding: EdgeInsets.all(16 * context.sf), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * context.sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * context.sf)]), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: _buildTeamInfo(myTeam, AppTheme.primaryRed, myTeamLogo, sf, textColor)), - _buildScoreCenter(context, gameId, sf, textColor), - Expanded(child: _buildTeamInfo(opponentTeam, Colors.grey.shade600, opponentTeamLogo, sf, textColor)), + Expanded(child: _buildTeamInfo(context, myTeam, const Color(0xFFE74C3C), myTeamLogo)), + _buildScoreCenter(context, gameId), + Expanded(child: _buildTeamInfo(context, opponentTeam, Colors.black87, opponentTeamLogo)), ], ), ); } - Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) { + Widget _buildTeamInfo(BuildContext context, String name, Color color, String? logoUrl) { return Column( children: [ - 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), + CircleAvatar(radius: 24 * context.sf, backgroundColor: color, backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * context.sf) : null), + SizedBox(height: 6 * context.sf), + Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2), ], ); } - Widget _buildScoreCenter(BuildContext context, String id, double sf, Color textColor) { + Widget _buildScoreCenter(BuildContext context, String id) { return Column( children: [ Row( mainAxisSize: MainAxisSize.min, children: [ - _scoreBox(myScore, AppTheme.successGreen, sf), - Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf, color: textColor)), - _scoreBox(opponentScore, Colors.grey, sf), + _scoreBox(context, myScore, Colors.green), + Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * context.sf)), + _scoreBox(context, opponentScore, Colors.grey), ], ), - SizedBox(height: 10 * sf), + SizedBox(height: 10 * context.sf), TextButton.icon( onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))), - icon: Icon(Icons.play_circle_fill, size: 18 * 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), + icon: Icon(Icons.play_circle_fill, size: 18 * context.sf, color: const Color(0xFFE74C3C)), + label: Text("RETORNAR", style: TextStyle(fontSize: 11 * context.sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)), + style: TextButton.styleFrom(backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), padding: EdgeInsets.symmetric(horizontal: 14 * context.sf, vertical: 8 * context.sf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), visualDensity: VisualDensity.compact), ), - SizedBox(height: 6 * sf), - Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)), + SizedBox(height: 6 * context.sf), + Text(status, style: TextStyle(fontSize: 12 * context.sf, color: Colors.blue, fontWeight: FontWeight.bold)), ], ); } - Widget _scoreBox(String pts, Color c, double sf) => Container( - padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf), - decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)), - child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)), + Widget _scoreBox(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)), ); } @@ -93,9 +79,8 @@ 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, required this.sf}); + const CreateGameDialogManual({super.key, required this.teamController, required this.gameController}); @override State createState() => _CreateGameDialogManualState(); @@ -121,29 +106,24 @@ class _CreateGameDialogManualState extends State { @override Widget build(BuildContext context) { return AlertDialog( - 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)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), + title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ - TextField( - controller: _seasonController, - style: TextStyle(fontSize: 14 * 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), + TextField(controller: _seasonController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * context.sf))), + SizedBox(height: 15 * context.sf), _buildSearch(context, "Minha Equipa", _myTeamController), - Padding(padding: EdgeInsets.symmetric(vertical: 10 * widget.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * widget.sf))), + Padding(padding: EdgeInsets.symmetric(vertical: 10 * context.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * context.sf))), _buildSearch(context, "Adversário", _opponentController), ], ), ), actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))), + TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * context.sf))), ElevatedButton( - 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)), + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)), onPressed: _isLoading ? null : () async { if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) { setState(() => _isLoading = true); @@ -155,7 +135,7 @@ class _CreateGameDialogManualState extends State { } } }, - 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)), + 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)), ), ], ); @@ -177,10 +157,9 @@ class _CreateGameDialogManualState extends State { return Align( alignment: Alignment.topLeft, child: Material( - color: Theme.of(context).colorScheme.surface, - elevation: 4.0, borderRadius: BorderRadius.circular(8 * widget.sf), + elevation: 4.0, borderRadius: BorderRadius.circular(8 * context.sf), child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: 250 * widget.sf, maxWidth: MediaQuery.of(context).size.width * 0.7), + constraints: BoxConstraints(maxHeight: 250 * context.sf, maxWidth: MediaQuery.of(context).size.width * 0.7), child: ListView.builder( padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, itemBuilder: (BuildContext context, int index) { @@ -188,8 +167,8 @@ class _CreateGameDialogManualState extends State { final String name = option['name'].toString(); final String? imageUrl = option['image_url']; return ListTile( - 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)), + leading: CircleAvatar(radius: 20 * context.sf, backgroundColor: Colors.grey.shade200, backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * context.sf) : null), + title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), onTap: () { onSelected(option); }, ); }, @@ -202,9 +181,8 @@ 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 * 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)), + 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()), ); }, ); @@ -224,24 +202,20 @@ 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: Theme.of(context).scaffoldBackgroundColor, + backgroundColor: Colors.white, elevation: 0, actions: [ Padding( padding: EdgeInsets.only(right: 8.0 * context.sf), child: IconButton( - icon: Icon(isFilterActive ? Icons.filter_list_alt : Icons.filter_list, color: isFilterActive ? AppTheme.primaryRed : Theme.of(context).colorScheme.onSurface, size: 26 * context.sf), + icon: Icon(isFilterActive ? Icons.filter_list_alt : Icons.filter_list, color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87, size: 26 * context.sf), onPressed: () => _showFilterPopup(context), ), ) @@ -251,28 +225,40 @@ class _GamePageState extends State { stream: teamController.teamsStream, builder: (context, teamSnapshot) { final List> teamsList = teamSnapshot.data ?? []; + + // 2º STREAM: Lemos os jogos return StreamBuilder>( - stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason), + stream: gameController.gamesStream, builder: (context, gameSnapshot) { if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) return const Center(child: CircularProgressIndicator()); - if (gameSnapshot.hasError) return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * context.sf, color: Theme.of(context).colorScheme.onSurface))); + if (gameSnapshot.hasError) return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * context.sf))); if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) { - return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.search_off, size: 48 * context.sf, color: Colors.grey.withOpacity(0.3)), SizedBox(height: 10 * context.sf), Text("Nenhum jogo encontrado.", style: TextStyle(fontSize: 14 * context.sf, color: Colors.grey))])); + 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 ListView.builder( - padding: EdgeInsets.all(16 * context.sf), + padding: const EdgeInsets.all(16), itemCount: gameSnapshot.data!.length, itemBuilder: (context, index) { final game = gameSnapshot.data![index]; - String? myLogo, oppLogo; + + // --- LÓGICA PARA ENCONTRAR A IMAGEM PELO NOME --- + String? myLogo; + String? oppLogo; + for (var team in teamsList) { - if (team['name'] == game.myTeam) myLogo = team['image_url']; - if (team['name'] == game.opponentTeam) oppLogo = team['image_url']; + if (team['name'] == game.myTeam) { + myLogo = team['image_url']; + } + if (team['name'] == game.opponentTeam) { + oppLogo = team['image_url']; + } } + + // Agora já passamos as imagens para o cartão! return GameResultCard( gameId: game.id, myTeam: game.myTeam, opponentTeam: game.opponentTeam, myScore: game.myScore, opponentScore: game.opponentScore, status: game.status, season: game.season, myTeamLogo: myLogo, opponentTeamLogo: oppLogo, - sf: context.sf, ); }, ); @@ -281,53 +267,49 @@ class _GamePageState extends State { }, ), floatingActionButton: FloatingActionButton( - heroTag: 'add_game_btn', - backgroundColor: AppTheme.primaryRed, + heroTag: 'add_game_btn', // 👇 A MÁGICA ESTÁ AQUI TAMBÉM! + backgroundColor: const Color(0xFFE74C3C), child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf), - onPressed: () => showDialog(context: context, builder: (context) => CreateGameDialogManual(teamController: teamController, gameController: gameController, sf: context.sf)), + onPressed: () => showDialog(context: context, builder: (context) => CreateGameDialogManual(teamController: teamController, gameController: gameController)), ), ); } - void _showFilterPopup(BuildContext context) { - String tempSeason = selectedSeason; - String tempTeam = selectedTeam; + void _showCreateDialog(BuildContext context) { showDialog( context: context, builder: (context) { return StatefulBuilder( builder: (context, setPopupState) { return AlertDialog( - 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, color: Theme.of(context).colorScheme.onSurface)), + Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)), IconButton(icon: const Icon(Icons.close, color: Colors.grey), onPressed: () => Navigator.pop(context), padding: EdgeInsets.zero, constraints: const BoxConstraints()) ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Temporada", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)), + Text("Temporada", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), SizedBox(height: 6 * context.sf), Container( - padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(10 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))), + padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)), child: DropdownButtonHideUnderline( child: DropdownButton( - 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), + isExpanded: true, value: tempSeason, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold), items: ['Todas', '2024/25', '2025/26'].map((String value) => DropdownMenuItem(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, fontWeight: FontWeight.bold)), + Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), SizedBox(height: 6 * context.sf), Container( - padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(10 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))), + padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)), child: StreamBuilder>>( stream: teamController.teamsStream, builder: (context, snapshot) { @@ -336,8 +318,7 @@ class _GamePageState extends State { if (!teamNames.contains(tempTeam)) tempTeam = 'Todas'; return DropdownButtonHideUnderline( child: DropdownButton( - 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), + isExpanded: true, value: tempTeam, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold), items: teamNames.map((String value) => DropdownMenuItem(value: value, child: Text(value, overflow: TextOverflow.ellipsis))).toList(), onChanged: (newValue) => setPopupState(() => tempTeam = newValue!), ), @@ -349,7 +330,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: 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))), + 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))), ], ); } diff --git a/lib/pages/teamPage.dart b/lib/pages/teamPage.dart index 23b3484..1e13a12 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'; +import '../utils/size_extension.dart'; // 👇 IMPORTANTE: O TEU NOVO SUPERPODER class TeamsPage extends StatefulWidget { const TeamsPage({super.key}); @@ -26,6 +26,7 @@ class _TeamsPageState extends State { super.dispose(); } + // --- POPUP DE FILTROS --- void _showFilterDialog(BuildContext context) { showDialog( context: context, @@ -33,14 +34,14 @@ class _TeamsPageState extends State { return StatefulBuilder( builder: (context, setModalState) { return AlertDialog( - backgroundColor: Theme.of(context).colorScheme.surface, + backgroundColor: const Color(0xFF2C3E50), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("Filtros de pesquisa", style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), + Text("Filtros de pesquisa", style: TextStyle(color: Colors.white, fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), IconButton( - icon: Icon(Icons.close, color: Colors.grey, size: 20 * context.sf), + icon: Icon(Icons.close, color: Colors.white, size: 20 * context.sf), onPressed: () => Navigator.pop(context), ) ], @@ -48,11 +49,12 @@ class _TeamsPageState extends State { content: Column( mainAxisSize: MainAxisSize.min, children: [ - Divider(color: Colors.grey.withOpacity(0.2)), + const Divider(color: Colors.white24), SizedBox(height: 16 * context.sf), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Coluna Temporada Expanded( child: _buildPopupColumn( title: "TEMPORADA", @@ -64,7 +66,8 @@ class _TeamsPageState extends State { }, ), ), - SizedBox(width: 20 * context.sf), + const SizedBox(width: 20), + // Coluna Ordenar Expanded( child: _buildPopupColumn( title: "ORDENAR POR", @@ -83,7 +86,7 @@ class _TeamsPageState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text("CONCLUÍDO", style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), + child: Text("CONCLUÍDO", style: TextStyle(color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), ), ], ); @@ -93,24 +96,28 @@ 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: TextStyle(color: Colors.grey, fontSize: 11 * context.sf, fontWeight: FontWeight.bold)), - SizedBox(height: 12 * context.sf), + Text(title, style: const TextStyle(color: Colors.grey, fontSize: 11, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), ...options.map((opt) { final isSelected = currentValue == opt; return InkWell( onTap: () => onSelect(opt), child: Padding( - padding: EdgeInsets.symmetric(vertical: 8.0 * context.sf), + padding: const EdgeInsets.symmetric(vertical: 8.0), child: Text( opt, style: TextStyle( - color: isSelected ? AppTheme.primaryRed : Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + color: isSelected ? const Color(0xFFE74C3C) : Colors.white70, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - fontSize: 14 * context.sf, ), ), ), @@ -126,11 +133,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: Theme.of(context).scaffoldBackgroundColor, + backgroundColor: const Color(0xFFF5F7FA), elevation: 0, actions: [ IconButton( - icon: Icon(Icons.filter_list, color: AppTheme.primaryRed, size: 24 * context.sf), + icon: Icon(Icons.filter_list, color: const Color(0xFFE74C3C), size: 24 * context.sf), onPressed: () => _showFilterDialog(context), ), ], @@ -142,8 +149,8 @@ class _TeamsPageState extends State { ], ), floatingActionButton: FloatingActionButton( - heroTag: 'add_team_btn', - backgroundColor: AppTheme.primaryRed, + heroTag: 'add_team_btn', // 👇 A MÁGICA ESTÁ AQUI! + backgroundColor: const Color(0xFFE74C3C), child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf), onPressed: () => _showCreateDialog(context), ), @@ -152,17 +159,17 @@ class _TeamsPageState extends State { Widget _buildSearchBar() { return Padding( - padding: EdgeInsets.all(16.0 * context.sf), + padding: const EdgeInsets.all(16.0), child: TextField( controller: _searchController, onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()), - style: TextStyle(fontSize: 16 * context.sf, color: Theme.of(context).colorScheme.onSurface), + style: TextStyle(fontSize: 16 * context.sf), decoration: InputDecoration( hintText: 'Pesquisar equipa...', - hintStyle: TextStyle(fontSize: 16 * context.sf, color: Colors.grey), - prefixIcon: Icon(Icons.search, color: AppTheme.primaryRed, size: 22 * context.sf), + hintStyle: TextStyle(fontSize: 16 * context.sf), + prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * context.sf), filled: true, - fillColor: Theme.of(context).colorScheme.surface, // 👇 Adapta-se ao Dark Mode + fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * context.sf), borderSide: BorderSide.none), ), ), @@ -173,30 +180,51 @@ class _TeamsPageState extends State { return StreamBuilder>>( stream: controller.teamsStream, builder: (context, snapshot) { - 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))); + 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))); var data = List>.from(snapshot.data!); - if (_selectedSeason != 'Todas') data = data.where((t) => t['season'] == _selectedSeason).toList(); - if (_searchQuery.isNotEmpty) data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList(); + // --- 1. FILTROS --- + if (_selectedSeason != 'Todas') { + data = data.where((t) => t['season'] == _selectedSeason).toList(); + } + if (_searchQuery.isNotEmpty) { + data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList(); + } + // --- 2. ORDENAÇÃO (FAVORITOS PRIMEIRO) --- data.sort((a, b) { + // Apanhar o estado de favorito (tratando null como false) bool favA = a['is_favorite'] ?? false; bool favB = b['is_favorite'] ?? false; - if (favA && !favB) return -1; - if (!favA && favB) return 1; - if (_currentSort == 'Nome') return a['name'].toString().compareTo(b['name'].toString()); - else return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString()); + + // REGRA 1: Favoritos aparecem sempre primeiro + if (favA && !favB) return -1; // A sobe + if (!favA && favB) return 1; // B sobe + + // REGRA 2: Se o estado de favorito for igual, aplica o filtro do utilizador + if (_currentSort == 'Nome') { + return a['name'].toString().compareTo(b['name'].toString()); + } else { // Recentes + return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString()); + } }); return ListView.builder( - padding: EdgeInsets.symmetric(horizontal: 16 * context.sf), + padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: data.length, itemBuilder: (context, index) { final team = Team.fromMap(data[index]); + + // Navegação para estatísticas return GestureDetector( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => TeamStatsPage(team: team)), + ); + }, child: TeamCard( team: team, controller: controller, @@ -211,7 +239,7 @@ class _TeamsPageState extends State { } void _showCreateDialog(BuildContext context) { - showDialog(context: context, builder: (context) => CreateTeamDialog(sf: context.sf, onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl))); + showDialog(context: context, builder: (context) => CreateTeamDialog(onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl))); } } @@ -220,140 +248,73 @@ 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, - required this.sf, - }); + const TeamCard({super.key, required this.team, required this.controller, required this.onFavoriteTap}); @override Widget build(BuildContext context) { - 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: [ - 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), - ), - ], + 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, ), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, + Positioned( + left: -15 * context.sf, top: -10 * context.sf, + child: IconButton( + icon: Icon(team.isFavorite ? Icons.star : Icons.star_border, color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1), size: 28 * context.sf, shadows: [Shadow(color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), blurRadius: 4 * context.sf)]), + onPressed: onFavoriteTap, + ), + ), + ], + ), + title: Text(team.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * context.sf), overflow: TextOverflow.ellipsis), + subtitle: Padding( + padding: EdgeInsets.only(top: 6.0 * context.sf), + child: Row( children: [ - 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), + 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)); + }, ), + 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, double sf, Color cardColor, Color textColor) { + void _confirmDelete(BuildContext context) { showDialog( context: context, builder: (context) => AlertDialog( - 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)), + title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), + content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * context.sf)), actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf, color: Colors.grey)), - ), - TextButton( - onPressed: () { - controller.deleteTeam(team.id); - Navigator.pop(context); - }, - child: Text('Eliminar', style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf)), - ), + 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))), ], ), ); @@ -363,9 +324,7 @@ class TeamCard extends StatelessWidget { // --- DIALOG DE CRIAÇÃO --- class CreateTeamDialog extends StatefulWidget { final Function(String name, String season, String imageUrl) onConfirm; - final double sf; - - const CreateTeamDialog({super.key, required this.onConfirm, required this.sf}); + const CreateTeamDialog({super.key, required this.onConfirm}); @override State createState() => _CreateTeamDialogState(); @@ -379,33 +338,31 @@ class _CreateTeamDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - 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)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)), + title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ - TextField(controller: _nameController, style: TextStyle(fontSize: 14 * 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), + TextField(controller: _nameController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * context.sf)), textCapitalization: TextCapitalization.words), + SizedBox(height: 15 * context.sf), DropdownButtonFormField( - 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), + value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf)), + style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87), items: ['2023/24', '2024/25', '2025/26'].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), onChanged: (val) => setState(() => _selectedSeason = val!), ), - SizedBox(height: 15 * 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))), + SizedBox(height: 15 * context.sf), + TextField(controller: _imageController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'URL Imagem ou Emoji', labelStyle: TextStyle(fontSize: 14 * context.sf), hintText: 'Ex: 🏀 ou https://...', hintStyle: TextStyle(fontSize: 14 * context.sf))), ], ), ), actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))), + TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))), ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)), + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)), onPressed: () { if (_nameController.text.trim().isNotEmpty) { widget.onConfirm(_nameController.text.trim(), _selectedSeason, _imageController.text.trim()); Navigator.pop(context); } }, - child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)), + child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * context.sf)), ), ], ); diff --git a/lib/screens/game_screen.dart b/lib/screens/game_screen.dart new file mode 100644 index 0000000..a47f241 --- /dev/null +++ b/lib/screens/game_screen.dart @@ -0,0 +1 @@ +import 'package:flutter/material.dart'; diff --git a/lib/utils/size_extension.dart b/lib/utils/size_extension.dart index a0ff6f1..b9f78bb 100644 --- a/lib/utils/size_extension.dart +++ b/lib/utils/size_extension.dart @@ -1,15 +1 @@ -import 'package:flutter/material.dart'; -import 'dart:math' as math; - -// Esta extensão adiciona o superpoder "sf" ao BuildContext -extension SizeExtension on BuildContext { - - double get sf { - final double wScreen = MediaQuery.of(this).size.width; - final double hScreen = MediaQuery.of(this).size.height; - - // Calcula e devolve a escala na hora! - return math.min(wScreen, hScreen) / 400; - } - -} \ No newline at end of file +// TODO Implement this library. \ No newline at end of file diff --git a/lib/widgets/game_widgets.dart b/lib/widgets/game_widgets.dart index c073376..5f723a8 100644 --- a/lib/widgets/game_widgets.dart +++ b/lib/widgets/game_widgets.dart @@ -1,18 +1,27 @@ 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, myTeam, opponentTeam, myScore, opponentScore, status, season; - final String? myTeamLogo, opponentTeamLogo; - final double sf; + 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 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, + 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 }); @override @@ -22,76 +31,291 @@ class GameResultCard extends StatelessWidget { final textColor = Theme.of(context).colorScheme.onSurface; return Container( - margin: EdgeInsets.only(bottom: 16 * sf), - padding: EdgeInsets.all(16 * sf), + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: bgColor, // Usa a cor do tema + color: Colors.white, borderRadius: BorderRadius.circular(20 * sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: _buildTeamInfo(myTeam, AppTheme.primaryRed, myTeamLogo, sf, textColor)), // Usa o primaryRed + Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, sf)), _buildScoreCenter(context, gameId, sf), - Expanded(child: _buildTeamInfo(opponentTeam, textColor, opponentTeamLogo, sf, textColor)), + Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, sf)), ], ), ); } - Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) { + Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf) { return Column( children: [ CircleAvatar( - radius: 24 * sf, + radius: 24 * sf, // Ajuste do tamanho do logo backgroundColor: color, - backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, - child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * sf) : null, + 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), + const SizedBox(height: 4), Text(name, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf, color: textColor), // Adapta à noite/dia - textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2, + 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 ), ], ); } 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, AppTheme.successGreen, sf), // Verde do tema - Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf, color: textColor)), + _scoreBox(myScore, Colors.green, sf), + Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf)), _scoreBox(opponentScore, Colors.grey, sf), ], ), - SizedBox(height: 10 * sf), + const SizedBox(height: 8), 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: AppTheme.primaryRed), - label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: AppTheme.primaryRed, 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: const Color(0xFFE74C3C)), + label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)), style: TextButton.styleFrom( - backgroundColor: AppTheme.primaryRed.withOpacity(0.1), + backgroundColor: const Color(0xFFE74C3C).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 * sf), - Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Text(status, style: const TextStyle(fontSize: 10, color: Colors.blue, fontWeight: FontWeight.bold)), ], ); } - Widget _scoreBox(String pts, Color c, double sf) => Container( - padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf), - decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)), - child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)), + Widget _scoreBox(String pts, Color c) => Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8)), + child: Text(pts, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), ); +} + +// --- 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 8880f20..04b3f48 100644 --- a/lib/widgets/home_widgets.dart +++ b/lib/widgets/home_widgets.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:playmaker/classe/home.config.dart'; class StatCard extends StatelessWidget { final String title; @@ -9,11 +10,6 @@ class StatCard extends StatelessWidget { final IconData icon; final bool isHighlighted; final VoidCallback? onTap; - - // Variáveis novas para que o tamanho não fique preso à HomeConfig - final double sf; - final double cardWidth; - final double cardHeight; const StatCard({ super.key, @@ -25,30 +21,27 @@ class StatCard extends StatelessWidget { required this.icon, this.isHighlighted = false, this.onTap, - this.sf = 1.0, // Default 1.0 para não dar erro se não passares o valor - required this.cardWidth, - required this.cardHeight, }); @override Widget build(BuildContext context) { - return SizedBox( - width: cardWidth, - height: cardHeight, + return Container( + width: HomeConfig.cardwidthPadding, + height: HomeConfig.cardheightPadding, child: Card( elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20 * sf), + borderRadius: BorderRadius.circular(20), side: isHighlighted - ? BorderSide(color: Colors.amber, width: 2 * sf) + ? BorderSide(color: Colors.amber, width: 2) : BorderSide.none, ), child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(20 * sf), + borderRadius: BorderRadius.circular(20), child: Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20 * sf), + borderRadius: BorderRadius.circular(20), gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, @@ -59,14 +52,13 @@ class StatCard extends StatelessWidget { ), ), child: Padding( - padding: EdgeInsets.all(16.0 * sf), + padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Cabeçalho Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( @@ -74,12 +66,12 @@ class StatCard extends StatelessWidget { children: [ Text( title.toUpperCase(), - style: TextStyle(fontSize: 11 * sf, fontWeight: FontWeight.bold, color: Colors.white70), + style: HomeConfig.titleStyle, ), - SizedBox(height: 2 * sf), + SizedBox(height: 5), Text( playerName, - style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold, color: Colors.white), + style: HomeConfig.playerNameStyle, maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -88,38 +80,38 @@ class StatCard extends StatelessWidget { ), if (isHighlighted) Container( - padding: EdgeInsets.all(6 * sf), - decoration: const BoxDecoration( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( color: Colors.amber, shape: BoxShape.circle, ), child: Icon( Icons.star, - size: 16 * sf, + size: 20, color: Colors.white, ), ), ], ), - SizedBox(height: 8 * sf), + SizedBox(height: 10), // Ícone Container( - width: 45 * sf, - height: 45 * sf, + width: 60, + height: 60, decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), shape: BoxShape.circle, ), child: Icon( icon, - size: 24 * sf, + size: 30, color: Colors.white, ), ), - const Spacer(), + Spacer(), // Estatística Center( @@ -127,26 +119,26 @@ class StatCard extends StatelessWidget { children: [ Text( statValue, - style: TextStyle(fontSize: 34 * sf, fontWeight: FontWeight.bold, color: Colors.white), + style: HomeConfig.statValueStyle, ), - SizedBox(height: 2 * sf), + SizedBox(height: 5), Text( statLabel.toUpperCase(), - style: TextStyle(fontSize: 12 * sf, color: Colors.white70), + style: HomeConfig.statLabelStyle, ), ], ), ), - const Spacer(), + Spacer(), // Botão Container( width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 8 * sf), + padding: EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(10 * sf), + borderRadius: BorderRadius.circular(15), ), child: Center( child: Text( @@ -154,7 +146,7 @@ class StatCard extends StatelessWidget { style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, - fontSize: 11 * sf, + fontSize: 14, letterSpacing: 1, ), ), @@ -177,12 +169,12 @@ class SportGrid extends StatelessWidget { const SportGrid({ super.key, required this.children, - this.spacing = 20.0, // Valor padrão se não for passado nada + this.spacing = HomeConfig.cardSpacing, }); @override Widget build(BuildContext context) { - if (children.isEmpty) return const SizedBox(); + if (children.isEmpty) return SizedBox(); return Column( children: [ diff --git a/lib/widgets/placar_widgets.dart b/lib/widgets/placar_widgets.dart index ad3d5c0..4f5cd37 100644 --- a/lib/widgets/placar_widgets.dart +++ b/lib/widgets/placar_widgets.dart @@ -7,104 +7,70 @@ import 'package:playmaker/zone_map_dialog.dart'; // ============================================================================ class TopScoreboard extends StatelessWidget { final PlacarController controller; - final double sf; - - const TopScoreboard({super.key, required this.controller, required this.sf}); + const TopScoreboard({super.key, required this.controller}); @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.symmetric(vertical: 10 * sf, horizontal: 35 * sf), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 30), decoration: BoxDecoration( - color: const Color(0xFF16202C), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(22 * sf), - bottomRight: Radius.circular(22 * sf) - ), - border: Border.all(color: Colors.white, width: 2.5 * sf), + color: const Color(0xFF16202C), + borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(15), bottomRight: Radius.circular(15)), + border: Border.all(color: Colors.white, width: 2), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - _buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false, sf), - SizedBox(width: 30 * sf), + _buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false), + const SizedBox(width: 25), Column( - mainAxisSize: MainAxisSize.min, children: [ Container( - padding: EdgeInsets.symmetric(horizontal: 18 * sf, vertical: 5 * sf), - decoration: BoxDecoration( - color: const Color(0xFF2C3E50), - borderRadius: BorderRadius.circular(9 * sf) - ), - child: Text( - controller.formatTime(), - style: TextStyle(color: Colors.white, fontSize: 28 * sf, fontWeight: FontWeight.w900, fontFamily: 'monospace', letterSpacing: 2 * sf) - ), - ), - SizedBox(height: 5 * sf), - Text( - "PERÍODO ${controller.currentQuarter}", - style: TextStyle(color: Colors.orangeAccent, fontSize: 14 * sf, fontWeight: FontWeight.w900) + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6), + decoration: BoxDecoration(color: const Color(0xFF2C3E50), borderRadius: BorderRadius.circular(6)), + child: Text(controller.formatTime(), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, fontFamily: 'monospace')), ), + const SizedBox(height: 5), + Text("PERÍODO ${controller.currentQuarter}", style: const TextStyle(color: Colors.orangeAccent, fontSize: 14, fontWeight: FontWeight.bold)), ], ), - SizedBox(width: 30 * sf), - _buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true, sf), + const SizedBox(width: 25), + _buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true), ], ), ); } - Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp, double sf) { - int displayFouls = fouls > 5 ? 5 : fouls; - + Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp) { final timeoutIndicators = Row( mainAxisSize: MainAxisSize.min, children: List.generate(3, (index) => Container( - margin: EdgeInsets.symmetric(horizontal: 3.5 * sf), - width: 12 * sf, height: 12 * sf, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: index < timeouts ? Colors.yellow : Colors.grey.shade600, - border: Border.all(color: Colors.white54, width: 1.5 * sf) - ), + margin: const EdgeInsets.symmetric(horizontal: 3), + width: 12, height: 12, + decoration: BoxDecoration(shape: BoxShape.circle, color: index < timeouts ? Colors.yellow : Colors.grey.shade600, border: Border.all(color: Colors.black26)), )), ); - - List content = [ - Column( - children: [ - _scoreBox(score, color, sf), - SizedBox(height: 7 * sf), - timeoutIndicators - ] - ), - SizedBox(width: 18 * sf), - Column( - crossAxisAlignment: isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end, - children: [ - Text( - name.toUpperCase(), - style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.2 * sf) - ), - SizedBox(height: 5 * sf), - Text( - "FALTAS: $displayFouls", - style: TextStyle(color: displayFouls >= 5 ? Colors.redAccent : Colors.yellowAccent, fontSize: 13 * sf, fontWeight: FontWeight.bold) - ), - ], - ) - ]; - - return Row(crossAxisAlignment: CrossAxisAlignment.center, children: isOpp ? content : content.reversed.toList()); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: isOpp + ? [ + Column(children: [_scoreBox(score, color), const SizedBox(height: 4), Text("FALTAS: $fouls", style: TextStyle(color: fouls >= 5 ? Colors.red : Colors.yellowAccent, fontSize: 12, fontWeight: FontWeight.bold)), timeoutIndicators]), + const SizedBox(width: 15), + Text(name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)) + ] + : [ + Text(name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(width: 15), + Column(children: [_scoreBox(score, color), const SizedBox(height: 4), Text("FALTAS: $fouls", style: TextStyle(color: fouls >= 5 ? Colors.red : Colors.yellowAccent, fontSize: 12, fontWeight: FontWeight.bold)), timeoutIndicators]) + ] + ); } - Widget _scoreBox(int score, Color color, double sf) => Container( - width: 58 * sf, height: 45 * sf, + Widget _scoreBox(int score, Color color) => Container( + width: 50, height: 40, alignment: Alignment.center, - decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(7 * sf)), - child: Text(score.toString(), style: TextStyle(color: Colors.white, fontSize: 26 * sf, fontWeight: FontWeight.w900)), + decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(6)), + child: Text(score.toString(), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)), ); } @@ -114,9 +80,7 @@ class TopScoreboard extends StatelessWidget { class BenchPlayersList extends StatelessWidget { final PlacarController controller; final bool isOpponent; - final double sf; - - const BenchPlayersList({super.key, required this.controller, required this.isOpponent, required this.sf}); + const BenchPlayersList({super.key, required this.controller, required this.isOpponent}); @override Widget build(BuildContext context) { @@ -132,45 +96,24 @@ class BenchPlayersList extends StatelessWidget { final bool isFouledOut = fouls >= 5; Widget avatarUI = Container( - margin: EdgeInsets.only(bottom: 7 * sf), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 1.8 * sf), - boxShadow: [BoxShadow(color: Colors.black45, blurRadius: 5 * sf, offset: Offset(0, 2.5 * sf))] - ), + margin: const EdgeInsets.only(bottom: 5), child: CircleAvatar( - radius: 22 * sf, - backgroundColor: isFouledOut ? Colors.grey.shade800 : teamColor, - child: Text( - num, - style: TextStyle( - color: isFouledOut ? Colors.red.shade300 : Colors.white, - fontSize: 16 * sf, - fontWeight: FontWeight.bold, - decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none - ) - ), + backgroundColor: isFouledOut ? Colors.grey.shade700 : teamColor, + child: Text(num, style: TextStyle(color: isFouledOut ? Colors.red.shade300 : Colors.white, fontSize: 14, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), ), ); if (isFouledOut) { return GestureDetector( - onTap: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $playerName não pode voltar (Expulso).'), backgroundColor: Colors.red)), - child: avatarUI + onTap: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $playerName não pode voltar (Expulso).'), backgroundColor: Colors.red)), + child: avatarUI, ); } return Draggable( data: "$prefix$playerName", - feedback: Material( - color: Colors.transparent, - child: CircleAvatar( - radius: 28 * sf, - backgroundColor: teamColor, - child: Text(num, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18 * sf)) - ) - ), - childWhenDragging: Opacity(opacity: 0.5, child: SizedBox(width: 45 * sf, height: 45 * sf)), + feedback: Material(color: Colors.transparent, child: CircleAvatar(backgroundColor: teamColor, child: Text(num, style: const TextStyle(color: Colors.white)))), + childWhenDragging: const Opacity(opacity: 0.5, child: SizedBox(width: 40, height: 40)), child: avatarUI, ); }).toList(), @@ -188,9 +131,8 @@ class PlayerCourtCard extends StatelessWidget { final PlacarController controller; final String name; final bool isOpponent; - final double sf; - const PlayerCourtCard({super.key, required this.controller, required this.name, required this.isOpponent, required this.sf}); + const PlayerCourtCard({super.key, required this.controller, required this.name, required this.isOpponent}); @override Widget build(BuildContext context) { @@ -204,12 +146,12 @@ class PlayerCourtCard extends StatelessWidget { feedback: Material( color: Colors.transparent, child: Container( - padding: EdgeInsets.symmetric(horizontal: 18 * sf, vertical: 11 * sf), - decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(9 * sf)), - child: Text(name, style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(8)), + child: Text(name, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), ), ), - childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, name, stats, teamColor, false, false, sf)), + childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, name, stats, teamColor, false, false)), child: DragTarget( onAcceptWithDetails: (details) { final action = details.data; @@ -235,27 +177,30 @@ 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, sf); + return _playerCardUI(number, name, stats, teamColor, isSubbing, isActionHover); }, ), ); } 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.shade50 : Colors.white; + Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white; Color borderColor = isFouledOut ? Colors.redAccent : Colors.transparent; - if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = Colors.blue; } - else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = Colors.orange; } + if (isSubbing) { + bgColor = Colors.blue.shade50; borderColor = Colors.blue; + } else if (isActionHover && !isFouledOut) { + bgColor = Colors.orange.shade50; borderColor = Colors.orange; + } int fgm = stats["fgm"]!; int fga = stats["fga"]!; @@ -263,11 +208,10 @@ class PlayerCourtCard extends StatelessWidget { String displayName = name.length > 12 ? "${name.substring(0, 10)}..." : name; return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(11 * sf), - border: Border.all(color: borderColor, width: 1.8 * sf), - boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5 * sf, offset: Offset(2 * sf, 3.5 * sf))], + color: bgColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: borderColor, width: 2), + boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 3))], ), child: ClipRRect( borderRadius: BorderRadius.circular(9 * sf), @@ -288,10 +232,19 @@ 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) + ), ], ), ), @@ -308,14 +261,12 @@ class PlayerCourtCard extends StatelessWidget { // ============================================================================ class ActionButtonsPanel extends StatelessWidget { final PlacarController controller; - final double sf; - - const ActionButtonsPanel({super.key, required this.controller, required this.sf}); + const ActionButtonsPanel({super.key, required this.controller}); @override Widget build(BuildContext context) { - final double baseSize = 65 * sf; - final double feedSize = 82 * sf; + final double baseSize = 65 * sf; // Reduzido (Antes era 75) + final double feedSize = 82 * sf; // Reduzido (Antes era 95) final double gap = 7 * sf; return Row( @@ -323,119 +274,116 @@ class ActionButtonsPanel extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ _columnBtn([ - _dragAndTargetBtn("M1", Colors.redAccent, "miss_1", baseSize, feedSize, sf), - _dragAndTargetBtn("1", Colors.orange, "add_pts_1", baseSize, feedSize, sf), - _dragAndTargetBtn("1", Colors.orange, "sub_pts_1", baseSize, feedSize, sf, isX: true), - _dragAndTargetBtn("STL", Colors.green, "add_stl", baseSize, feedSize, sf), - ], gap), - SizedBox(width: gap * 1), + _actionBtn("T.O", const Color(0xFF1E5BB2), () => controller.useTimeout(false), labelSize: 20), + _dragAndTargetBtn("1", Colors.orange, "add_pts_1"), + _dragAndTargetBtn("1", Colors.orange, "sub_pts_1", isX: true), + _dragAndTargetBtn("STL", Colors.green, "add_stl"), + + ]), + const SizedBox(width: 15), _columnBtn([ - _dragAndTargetBtn("M2", Colors.redAccent, "miss_2", baseSize, feedSize, sf), - _dragAndTargetBtn("2", Colors.orange, "add_pts_2", baseSize, feedSize, sf), - _dragAndTargetBtn("2", Colors.orange, "sub_pts_2", baseSize, feedSize, sf, isX: true), - _dragAndTargetBtn("AST", Colors.blueGrey, "add_ast", baseSize, feedSize, sf), - ], gap), - SizedBox(width: gap * 1), + _dragAndTargetBtn("M2", Colors.redAccent, "miss_2"), + _dragAndTargetBtn("2", Colors.orange, "add_pts_2"), + _dragAndTargetBtn("2", Colors.orange, "sub_pts_2", isX: true), + _dragAndTargetBtn("AST", Colors.blueGrey, "add_ast"), + ]), + const SizedBox(width: 15), _columnBtn([ - _dragAndTargetBtn("M3", Colors.redAccent, "miss_3", baseSize, feedSize, sf), - _dragAndTargetBtn("3", Colors.orange, "add_pts_3", baseSize, feedSize, sf), - _dragAndTargetBtn("3", Colors.orange, "sub_pts_3", baseSize, feedSize, sf, isX: true), - _dragAndTargetBtn("TOV", Colors.redAccent, "add_tov", baseSize, feedSize, sf), - ], gap), - SizedBox(width: gap * 1), + _dragAndTargetBtn("M3", Colors.redAccent, "miss_3"), + _dragAndTargetBtn("3", Colors.orange, "add_pts_3"), + _dragAndTargetBtn("3", Colors.orange, "sub_pts_3", isX: true), + _dragAndTargetBtn("TOV", Colors.redAccent, "add_tov"), + ]), + const SizedBox(width: 15), _columnBtn([ - _dragAndTargetBtn("ORB", const Color(0xFF1E2A38), "add_orb", baseSize, feedSize, sf, icon: Icons.sports_basketball), - _dragAndTargetBtn("DRB", const Color(0xFF1E2A38), "add_drb", baseSize, feedSize, sf, icon: Icons.sports_basketball), - _dragAndTargetBtn("BLK", Colors.deepPurple, "add_blk", baseSize, feedSize, sf, icon: Icons.front_hand), - ], gap), + _actionBtn("T.O", const Color(0xFFD92C2C), () => controller.useTimeout(true), labelSize: 20), + _dragAndTargetBtn("ORB", const Color(0xFF1E2A38), "add_rbs", icon: Icons.sports_basketball), + _dragAndTargetBtn("DRB", const Color(0xFF1E2A38), "add_rbs", icon: Icons.sports_basketball), + + _dragAndTargetBtn("BLK", Colors.deepPurple, "add_blk", icon: Icons.front_hand), + ]), + const SizedBox(width: 15), + _columnBtn([ + ]) ], ); } - Widget _columnBtn(List children, double gap) { - return Column( - mainAxisSize: MainAxisSize.min, - children: children.map((c) => Padding(padding: EdgeInsets.only(bottom: gap), child: c)).toList() - ); - } + // Mantenha os métodos _columnBtn, _dragAndTargetBtn, _actionBtn e _circle exatamente como estão + Widget _columnBtn(List children) => Column(mainAxisSize: MainAxisSize.min, children: children.map((c) => Padding(padding: const EdgeInsets.only(bottom: 8), child: c)).toList()); - Widget _dragAndTargetBtn(String label, Color color, String actionData, double baseSize, double feedSize, double sf, {IconData? icon, bool isX = false}) { + Widget _dragAndTargetBtn(String label, Color color, String actionData, {IconData? icon, bool isX = false}) { return Draggable( data: actionData, - feedback: _circle(label, color, icon, true, baseSize, feedSize, sf, isX: isX), - childWhenDragging: Opacity( - opacity: 0.5, - child: _circle(label, color, icon, false, baseSize, feedSize, sf, isX: isX) - ), + feedback: _circle(label, color, icon, true, isX: isX), + childWhenDragging: Opacity(opacity: 0.5, child: _circle(label, color, icon, false, isX: isX)), child: DragTarget( - onAcceptWithDetails: (details) {}, // O PlayerCourtCard é que processa a ação! + onAcceptWithDetails: (details) {}, builder: (context, candidateData, rejectedData) { bool isHovered = candidateData.any((data) => data != null && data.startsWith("player_")); return Transform.scale( scale: isHovered ? 1.15 : 1.0, - child: Container( - decoration: isHovered ? BoxDecoration(shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.white, blurRadius: 10 * sf, spreadRadius: 3 * sf)]) : null, - child: _circle(label, color, icon, false, baseSize, feedSize, sf, isX: isX) - ), + child: Container(decoration: isHovered ? BoxDecoration(shape: BoxShape.circle, boxShadow: const [BoxShadow(color: Colors.white, blurRadius: 10, spreadRadius: 3)]) : null, child: _circle(label, color, icon, false, isX: isX)), ); } ), ); } - Widget _circle(String label, Color color, IconData? icon, bool isFeed, double baseSize, double feedSize, double sf, {bool isX = false}) { - double size = isFeed ? feedSize : baseSize; + Widget _actionBtn(String label, Color color, VoidCallback onTap, {IconData? icon, bool isX = false, double labelSize = 24}) { + return GestureDetector(onTap: onTap, child: _circle(label, color, icon, false, fontSize: labelSize, isX: isX)); + } + + Widget _circle(String label, Color color, IconData? icon, bool isFeed, {double fontSize = 20, bool isX = false}) { Widget content; - bool isPointBtn = label == "1" || label == "2" || label == "3" || label == "M1" || label == "M2" || label == "M3"; + bool isPointBtn = label == "1" || label == "2" || label == "3" || label == "M2" || label == "M3"; bool isBlkBtn = label == "BLK"; if (isPointBtn) { content = Stack( alignment: Alignment.center, children: [ - Container(width: size * 0.75, height: size * 0.75, decoration: const BoxDecoration(color: Colors.black, shape: BoxShape.circle)), - Icon(Icons.sports_basketball, color: color, size: size * 0.9), + Container(width: isFeed ? 55 : 45, height: isFeed ? 55 : 45, decoration: const BoxDecoration(color: Colors.black, shape: BoxShape.circle)), + Icon(Icons.sports_basketball, color: color, size: isFeed ? 65 : 55), Stack( children: [ - Text(label, style: TextStyle(fontSize: size * 0.38, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = size * 0.05..color = Colors.white, decoration: TextDecoration.none)), - Text(label, style: TextStyle(fontSize: size * 0.38, fontWeight: FontWeight.w900, color: Colors.black, decoration: TextDecoration.none)), + Text(label, style: TextStyle(fontSize: isFeed ? 26 : 22, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = 3..color = Colors.white, decoration: TextDecoration.none)), + Text(label, style: TextStyle(fontSize: isFeed ? 26 : 22, fontWeight: FontWeight.w900, color: Colors.black, decoration: TextDecoration.none)), ], ), ], ); - } else if (isBlkBtn) { + } + else if (isBlkBtn) { content = Stack( alignment: Alignment.center, children: [ - Icon(Icons.front_hand, color: const Color.fromARGB(207, 56, 52, 52), size: size * 0.75), + Icon(Icons.front_hand, color: const Color.fromARGB(207, 56, 52, 52), size: isFeed ? 55 : 45), Stack( alignment: Alignment.center, children: [ - Text(label, style: TextStyle(fontSize: size * 0.28, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = size * 0.05..color = Colors.black, decoration: TextDecoration.none)), - Text(label, style: TextStyle(fontSize: size * 0.28, fontWeight: FontWeight.w900, color: Colors.white, decoration: TextDecoration.none)), + Text(label, style: TextStyle(fontSize: isFeed ? 18 : 16, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = 3..color = Colors.black, decoration: TextDecoration.none)), + Text(label, style: TextStyle(fontSize: isFeed ? 18 : 16, fontWeight: FontWeight.w900, color: Colors.white, decoration: TextDecoration.none)), ], ), ], ); } else if (icon != null) { - content = Icon(icon, color: Colors.white, size: size * 0.5); + content = Icon(icon, color: Colors.white, size: 30); } else { - content = Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: size * 0.35, decoration: TextDecoration.none)); + content = Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: fontSize, decoration: TextDecoration.none)); } return Stack( - clipBehavior: Clip.none, - alignment: Alignment.bottomRight, + clipBehavior: Clip.none, alignment: Alignment.bottomRight, children: [ Container( width: size, height: size, - decoration: (isPointBtn || isBlkBtn) - ? const BoxDecoration(color: Colors.transparent) - : BoxDecoration(gradient: RadialGradient(colors: [color.withOpacity(0.7), color], radius: 0.8), shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black38, blurRadius: 6 * sf, offset: Offset(0, 3 * sf))]), + 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: size * 0.4))), + if (isX) Positioned(top: 0, right: 0, child: Container(decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), child: Icon(Icons.cancel, color: Colors.red, size: isFeed ? 28 : 24))), ], ); } diff --git a/lib/widgets/team_widgets.dart b/lib/widgets/team_widgets.dart index 3075c66..2b09247 100644 --- a/lib/widgets/team_widgets.dart +++ b/lib/widgets/team_widgets.dart @@ -7,61 +7,154 @@ 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 StatsHeader({super.key, required this.team}); + const TeamCard({ + super.key, + required this.team, + required this.controller, + required this.onFavoriteTap, + required this.sf, + }); @override Widget build(BuildContext 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) + 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, + ), + ), + ], ), - ), - child: Row( - children: [ - IconButton( - icon: Icon(Icons.arrow_back, color: Colors.white, size: 24 * context.sf), - onPressed: () => Navigator.pop(context), - ), - 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), + + // --- 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, ), - Text( - team.season, - style: TextStyle(color: Colors.white70, fontSize: 14 * context.sf) - ), - ], + ), + ], + ), + ), + + // --- 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), + ), + ], + ), + ), + ); + } + + 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( + 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)), ), ], ), @@ -69,164 +162,90 @@ class StatsHeader extends StatelessWidget { } } -// --- CARD DE RESUMO --- -class StatsSummaryCard extends StatelessWidget { - final int total; +// --- DIALOG DE CRIAÇÃO --- +class CreateTeamDialog extends StatefulWidget { + final Function(String name, String season, String imageUrl) onConfirm; + final double sf; // Recebe a escala - const StatsSummaryCard({super.key, required this.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'; @override Widget build(BuildContext context) { - // 👇 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, + 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, children: [ - 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 - ) - ), - ], + TextField( + controller: _nameController, + style: TextStyle(fontSize: 14 * widget.sf), + decoration: InputDecoration( + labelText: 'Nome da Equipa', + labelStyle: TextStyle(fontSize: 14 * widget.sf) + ), + textCapitalization: TextCapitalization.words, ), - Text( - "$total", - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável - fontSize: 28 * context.sf, - fontWeight: FontWeight.bold - ) + 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) + ), ), ], ), ), - ); - } -} - -// --- 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 - ) + 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)), ), - 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 diff --git a/pubspec.yaml b/pubspec.yaml index 590c58f..036a8bf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,7 +58,6 @@ flutter: assets: - assets/playmaker-logo.png - assets/campo.png - - assets/playmaker-logos.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images