diff --git a/assets/playmaker-logos.png b/assets/playmaker-logos.png new file mode 100644 index 0000000..6d08b0a Binary files /dev/null and b/assets/playmaker-logos.png differ diff --git a/lib/pages/PlacarPage.dart b/lib/pages/PlacarPage.dart index f654d42..b3d901e 100644 --- a/lib/pages/PlacarPage.dart +++ b/lib/pages/PlacarPage.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:playmaker/controllers/placar_controller.dart'; -import 'package:playmaker/widgets/placar_widgets.dart'; -import 'dart:math' as math; +import '../utils/size_extension.dart'; // 👇 EXTENSÃO IMPORTADA class PlacarPage extends StatefulWidget { final String gameId, myTeam, opponentTeam; @@ -42,30 +41,20 @@ class _PlacarPageState extends State { } // --- BOTÕES FLUTUANTES DE FALTA --- - Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top, double sf) { + Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top) { return Positioned( - top: top, - left: left > 0 ? left : null, - right: right > 0 ? right : null, + top: top, left: left > 0 ? left : null, right: right > 0 ? right : null, child: Draggable( data: action, feedback: Material( color: Colors.transparent, - child: CircleAvatar( - radius: 30 * sf, - backgroundColor: color.withOpacity(0.8), - child: Icon(icon, color: Colors.white, size: 30 * sf) - ), + child: CircleAvatar(radius: 30 * context.sf, backgroundColor: color.withOpacity(0.8), child: Icon(icon, color: Colors.white, size: 30 * context.sf)), ), child: Column( children: [ - CircleAvatar( - radius: 27 * sf, - backgroundColor: color, - child: Icon(icon, color: Colors.white, size: 28 * sf), - ), - SizedBox(height: 5 * sf), - Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 * sf)), + CircleAvatar(radius: 27 * context.sf, backgroundColor: color, child: Icon(icon, color: Colors.white, size: 28 * context.sf)), + SizedBox(height: 5 * context.sf), + Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 * context.sf)), ], ), ), @@ -75,16 +64,13 @@ class _PlacarPageState extends State { // --- BOTÕES LATERAIS QUADRADOS --- Widget _buildCornerBtn({required String heroTag, required IconData icon, required Color color, required VoidCallback onTap, required double size, bool isLoading = false}) { return SizedBox( - width: size, - height: size, + width: size, height: size, child: FloatingActionButton( - heroTag: heroTag, - backgroundColor: color, + heroTag: heroTag, backgroundColor: color, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * (size / 50))), - elevation: 5, - onPressed: isLoading ? null : onTap, + elevation: 5, onPressed: isLoading ? null : onTap, child: isLoading - ? SizedBox(width: size*0.45, height: size*0.45, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5)) + ? SizedBox(width: size*0.45, height: size*0.45, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5)) : Icon(icon, color: Colors.white, size: size * 0.55), ), ); @@ -95,10 +81,7 @@ class _PlacarPageState extends State { final double wScreen = MediaQuery.of(context).size.width; final double hScreen = MediaQuery.of(context).size.height; - // 👇 DIVISOR AUMENTADO PARA O 'sf' FICAR MAIS PEQUENO 👇 - final double sf = math.min(wScreen / 1150, hScreen / 720); - - final double cornerBtnSize = 48 * sf; // Tamanho ideal (Nem 38 nem 55) + final double cornerBtnSize = 48 * context.sf; if (_controller.isLoading) { return Scaffold( @@ -107,20 +90,18 @@ class _PlacarPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("PREPARANDO O PAVILHÃO", style: TextStyle(color: Colors.white24, fontSize: 45 * sf, fontWeight: FontWeight.bold, letterSpacing: 2)), - SizedBox(height: 35 * sf), + Text("PREPARANDO O PAVILHÃO", style: TextStyle(color: Colors.white24, fontSize: 45 * context.sf, fontWeight: FontWeight.bold, letterSpacing: 2)), + SizedBox(height: 35 * context.sf), StreamBuilder( stream: Stream.periodic(const Duration(seconds: 3)), builder: (context, snapshot) { List frases = [ - "O Treinador está a desenhar a tática...", - "A encher as bolas com ar de campeão...", - "O árbitro está a testar o apito...", - "A verificar se o cesto está nivelado...", + "O Treinador está a desenhar a tática...", "A encher as bolas com ar de campeão...", + "O árbitro está a testar o apito...", "A verificar se o cesto está nivelado...", "Os jogadores estão a terminar o aquecimento..." ]; String frase = frases[DateTime.now().second % frases.length]; - return Text(frase, style: TextStyle(color: Colors.orange.withOpacity(0.7), fontSize: 26 * sf, fontStyle: FontStyle.italic)); + return Text(frase, style: TextStyle(color: Colors.orange.withOpacity(0.7), fontSize: 26 * context.sf, fontStyle: FontStyle.italic)); }, ), ], @@ -132,13 +113,12 @@ class _PlacarPageState extends State { return Scaffold( backgroundColor: const Color(0xFF266174), body: SafeArea( - top: false, - bottom: false, + top: false, bottom: false, child: Stack( children: [ // --- O CAMPO --- Container( - margin: EdgeInsets.only(left: 65 * sf, right: 65 * sf, bottom: 55 * sf), + margin: EdgeInsets.only(left: 65 * context.sf, right: 65 * context.sf, bottom: 55 * context.sf), decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.5)), child: LayoutBuilder( builder: (context, constraints) { @@ -155,14 +135,12 @@ class _PlacarPageState extends State { }, child: Container( decoration: const BoxDecoration( -image: DecorationImage( - image: AssetImage('assets/campo.png'), - fit: BoxFit.fill, // <-- A MÁGICA ESTÁ AQUI -), ), + image: DecorationImage(image: AssetImage('assets/campo.png'), fit: BoxFit.fill), + ), child: Stack( children: _controller.matchShots.map((shot) => Positioned( - left: shot.position.dx - (9 * sf), top: shot.position.dy - (9 * sf), - child: CircleAvatar(radius: 9 * sf, backgroundColor: shot.isMake ? Colors.green : Colors.red, child: Icon(shot.isMake ? Icons.check : Icons.close, size: 11 * sf, color: Colors.white)), + left: shot.position.dx - (9 * context.sf), top: shot.position.dy - (9 * context.sf), + child: CircleAvatar(radius: 9 * context.sf, backgroundColor: shot.isMake ? Colors.green : Colors.red, child: Icon(shot.isMake ? Icons.check : Icons.close, size: 11 * context.sf, color: Colors.white)), )).toList(), ), ), @@ -170,47 +148,47 @@ image: DecorationImage( // --- JOGADORES --- if (!_controller.isSelectingShotLocation) ...[ - Positioned(top: h * 0.25, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[0], isOpponent: false, sf: sf)), - Positioned(top: h * 0.68, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[1], isOpponent: false, sf: sf)), - Positioned(top: h * 0.45, left: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[2], isOpponent: false, sf: sf)), - Positioned(top: h * 0.15, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[3], isOpponent: false, sf: sf)), - Positioned(top: h * 0.80, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[4], isOpponent: false, sf: sf)), + Positioned(top: h * 0.25, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[0], isOpponent: false)), + Positioned(top: h * 0.68, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[1], isOpponent: false)), + Positioned(top: h * 0.45, left: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[2], isOpponent: false)), + Positioned(top: h * 0.15, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[3], isOpponent: false)), + Positioned(top: h * 0.80, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[4], isOpponent: false)), - Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[0], isOpponent: true, sf: sf)), - Positioned(top: h * 0.68, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[1], isOpponent: true, sf: sf)), - Positioned(top: h * 0.45, right: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[2], isOpponent: true, sf: sf)), - Positioned(top: h * 0.15, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[3], isOpponent: true, sf: sf)), - Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[4], isOpponent: true, sf: sf)), + Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[0], isOpponent: true)), + Positioned(top: h * 0.68, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[1], isOpponent: true)), + Positioned(top: h * 0.45, right: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[2], isOpponent: true)), + Positioned(top: h * 0.15, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[3], isOpponent: true)), + Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[4], isOpponent: true)), ], // --- BOTÕES DE FALTAS --- if (!_controller.isSelectingShotLocation) ...[ - _buildFloatingFoulBtn("FALTA +", Colors.orange, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31, sf), - _buildFloatingFoulBtn("FALTA -", Colors.redAccent, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31, sf), + _buildFloatingFoulBtn("FALTA +", Colors.orange, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31), + _buildFloatingFoulBtn("FALTA -", Colors.redAccent, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31), ], // --- BOTÃO PLAY/PAUSE --- - if (!_controller.isSelectingShotLocation) - Positioned( + if (!_controller.isSelectingShotLocation) + Positioned( + top: (h * 0.32) + (40 * context.sf), + left: 0, right: 0, + child: Center( + child: GestureDetector( + onTap: () => _controller.toggleTimer(context), + child: CircleAvatar( + radius: 68 * context.sf, + backgroundColor: Colors.grey.withOpacity(0.5), + child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 58 * context.sf) + ), + ), + ), + ), - top: (h * 0.32) + (40 * sf), - left: 0, right: 0, - child: Center( - child: GestureDetector( - onTap: () => _controller.toggleTimer(context), - child: CircleAvatar( - radius: 68 * sf, - backgroundColor: Colors.grey.withOpacity(0.5), - child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 58 * sf) - ), - ), - ), - ), // --- PLACAR NO TOPO --- - Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))), + Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller))), // --- BOTÕES DE AÇÃO --- - if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller, sf: sf)), + if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * context.sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller)), // --- OVERLAY LANÇAMENTO --- if (_controller.isSelectingShotLocation) @@ -218,9 +196,9 @@ image: DecorationImage( top: h * 0.4, left: 0, right: 0, child: Center( child: Container( - padding: EdgeInsets.symmetric(horizontal: 35 * sf, vertical: 18 * sf), - decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(11 * sf), border: Border.all(color: Colors.white, width: 1.5 * sf)), - child: Text("TOQUE NO CAMPO PARA MARCAR O LOCAL DO LANÇAMENTO", style: TextStyle(color: Colors.white, fontSize: 27 * sf, fontWeight: FontWeight.bold)), + padding: EdgeInsets.symmetric(horizontal: 35 * context.sf, vertical: 18 * context.sf), + decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(11 * context.sf), border: Border.all(color: Colors.white, width: 1.5 * context.sf)), + child: Text("TOQUE NO CAMPO PARA MARCAR O LOCAL DO LANÇAMENTO", style: TextStyle(color: Colors.white, fontSize: 27 * context.sf, fontWeight: FontWeight.bold)), ), ), ), @@ -231,75 +209,378 @@ image: DecorationImage( ), // --- BOTÕES LATERAIS --- - // Topo Esquerdo: Guardar e Sair (Botão Único) -Positioned( - top: 50 * sf, left: 12 * sf, - child: _buildCornerBtn( - heroTag: 'btn_save_exit', - icon: Icons.save_alt, // Mudei o ícone para dar a ideia de "Guardar e Sair" - color: const Color(0xFFD92C2C), // Mantive vermelho para saberes que é para fechar - size: cornerBtnSize, - isLoading: _controller.isSaving, - onTap: () async { - // 1. Primeiro obriga a guardar os dados na BD - await _controller.saveGameStats(context); - - // 2. Só depois de acabar de guardar é que volta para trás (sai da página) - if (context.mounted) { - Navigator.pop(context); - } - } - ), -), - - // Base Esquerda: Banco Casa + TIMEOUT DA CASA - Positioned( - bottom: 55 * sf, left: 12 * sf, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (_controller.showMyBench) BenchPlayersList(controller: _controller, isOpponent: false, sf: sf), - SizedBox(height: 12 * sf), - _buildCornerBtn(heroTag: 'btn_sub_home', icon: Icons.swap_horiz, color: const Color(0xFF1E5BB2), size: cornerBtnSize, onTap: () { _controller.showMyBench = !_controller.showMyBench; _controller.onUpdate(); }), - SizedBox(height: 12 * sf), - _buildCornerBtn( - heroTag: 'btn_to_home', - icon: Icons.timer, - color: _controller.myTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFF1E5BB2), - size: cornerBtnSize, - onTap: _controller.myTimeoutsUsed >= 3 - ? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 A equipa da casa já usou os 3 Timeouts deste período!'), backgroundColor: Colors.red)) - : () => _controller.useTimeout(false) - ), - ], - ), + Positioned( + top: 50 * context.sf, left: 12 * context.sf, + child: _buildCornerBtn( + heroTag: 'btn_save_exit', icon: Icons.save_alt, color: const Color(0xFFD92C2C), size: cornerBtnSize, isLoading: _controller.isSaving, + onTap: () async { + await _controller.saveGameStats(context); + if (context.mounted) Navigator.pop(context); + } ), + ), - // Base Direita: Banco Visitante + TIMEOUT DO VISITANTE - Positioned( - bottom: 55 * sf, right: 12 * sf, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (_controller.showOppBench) BenchPlayersList(controller: _controller, isOpponent: true, sf: sf), - SizedBox(height: 12 * sf), - _buildCornerBtn(heroTag: 'btn_sub_away', icon: Icons.swap_horiz, color: const Color(0xFFD92C2C), size: cornerBtnSize, onTap: () { _controller.showOppBench = !_controller.showOppBench; _controller.onUpdate(); }), - SizedBox(height: 12 * sf), - _buildCornerBtn( - heroTag: 'btn_to_away', - icon: Icons.timer, - color: _controller.opponentTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFFD92C2C), - size: cornerBtnSize, - onTap: _controller.opponentTimeoutsUsed >= 3 - ? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 A equipa visitante já usou os 3 Timeouts deste período!'), backgroundColor: Colors.red)) - : () => _controller.useTimeout(true) - ), - ], - ), + // Base Esquerda: Banco Casa + TIMEOUT DA CASA + Positioned( + bottom: 55 * context.sf, left: 12 * context.sf, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_controller.showMyBench) BenchPlayersList(controller: _controller, isOpponent: false), + SizedBox(height: 12 * context.sf), + _buildCornerBtn(heroTag: 'btn_sub_home', icon: Icons.swap_horiz, color: const Color(0xFF1E5BB2), size: cornerBtnSize, onTap: () { _controller.showMyBench = !_controller.showMyBench; _controller.onUpdate(); }), + SizedBox(height: 12 * context.sf), + _buildCornerBtn( + heroTag: 'btn_to_home', icon: Icons.timer, color: _controller.myTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFF1E5BB2), size: cornerBtnSize, + onTap: _controller.myTimeoutsUsed >= 3 ? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 A equipa da casa já usou os 3 Timeouts deste período!'), backgroundColor: Colors.red)) : () => _controller.useTimeout(false) + ), + ], ), - ], + ), + + // Base Direita: Banco Visitante + TIMEOUT DO VISITANTE + Positioned( + bottom: 55 * context.sf, right: 12 * context.sf, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_controller.showOppBench) BenchPlayersList(controller: _controller, isOpponent: true), + SizedBox(height: 12 * context.sf), + _buildCornerBtn(heroTag: 'btn_sub_away', icon: Icons.swap_horiz, color: const Color(0xFFD92C2C), size: cornerBtnSize, onTap: () { _controller.showOppBench = !_controller.showOppBench; _controller.onUpdate(); }), + SizedBox(height: 12 * context.sf), + _buildCornerBtn( + heroTag: 'btn_to_away', icon: Icons.timer, color: _controller.opponentTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFFD92C2C), size: cornerBtnSize, + onTap: _controller.opponentTimeoutsUsed >= 3 ? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 A equipa visitante já usou os 3 Timeouts deste período!'), backgroundColor: Colors.red)) : () => _controller.useTimeout(true) + ), + ], + ), + ), + ], ), ), ); } +} + +// ========================================== +// WIDGETS DO PLACAR (Sem receber o `sf`) +// ========================================== + +class TopScoreboard extends StatelessWidget { + final PlacarController controller; + const TopScoreboard({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(vertical: 10 * context.sf, horizontal: 35 * context.sf), + decoration: BoxDecoration( + color: const Color(0xFF16202C), + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(22 * context.sf), bottomRight: Radius.circular(22 * context.sf)), + border: Border.all(color: Colors.white, width: 2.5 * context.sf), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildTeamSection(context, controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false), + SizedBox(width: 30 * context.sf), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 18 * context.sf, vertical: 5 * context.sf), + decoration: BoxDecoration(color: const Color(0xFF2C3E50), borderRadius: BorderRadius.circular(9 * context.sf)), + child: Text(controller.formatTime(), style: TextStyle(color: Colors.white, fontSize: 28 * context.sf, fontWeight: FontWeight.w900, fontFamily: 'monospace', letterSpacing: 2 * context.sf)), + ), + SizedBox(height: 5 * context.sf), + Text("PERÍODO ${controller.currentQuarter}", style: TextStyle(color: Colors.orangeAccent, fontSize: 14 * context.sf, fontWeight: FontWeight.w900)), + ], + ), + SizedBox(width: 30 * context.sf), + _buildTeamSection(context, controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true), + ], + ), + ); + } + + Widget _buildTeamSection(BuildContext context, String name, int score, int fouls, int timeouts, Color color, bool isOpp) { + int displayFouls = fouls > 5 ? 5 : fouls; + final timeoutIndicators = Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(3, (index) => Container( + margin: EdgeInsets.symmetric(horizontal: 3.5 * context.sf), + width: 12 * context.sf, height: 12 * context.sf, + decoration: BoxDecoration(shape: BoxShape.circle, color: index < timeouts ? Colors.yellow : Colors.grey.shade600, border: Border.all(color: Colors.white54, width: 1.5 * context.sf)), + )), + ); + + List content = [ + Column(children: [_scoreBox(context, score, color), SizedBox(height: 7 * context.sf), timeoutIndicators]), + SizedBox(width: 18 * context.sf), + Column( + crossAxisAlignment: isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end, + children: [ + Text(name.toUpperCase(), style: TextStyle(color: Colors.white, fontSize: 20 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.2 * context.sf)), + SizedBox(height: 5 * context.sf), + Text("FALTAS: $displayFouls", style: TextStyle(color: displayFouls >= 5 ? Colors.redAccent : Colors.yellowAccent, fontSize: 13 * context.sf, fontWeight: FontWeight.bold)), + ], + ) + ]; + + return Row(crossAxisAlignment: CrossAxisAlignment.center, children: isOpp ? content : content.reversed.toList()); + } + + Widget _scoreBox(BuildContext context, int score, Color color) => Container( + width: 58 * context.sf, height: 45 * context.sf, alignment: Alignment.center, + decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(7 * context.sf)), + child: Text(score.toString(), style: TextStyle(color: Colors.white, fontSize: 26 * context.sf, fontWeight: FontWeight.w900)), + ); +} + +class BenchPlayersList extends StatelessWidget { + final PlacarController controller; + final bool isOpponent; + const BenchPlayersList({super.key, required this.controller, required this.isOpponent}); + + @override + Widget build(BuildContext context) { + final bench = isOpponent ? controller.oppBench : controller.myBench; + final teamColor = isOpponent ? const Color(0xFFD92C2C) : const Color(0xFF1E5BB2); + final prefix = isOpponent ? "bench_opp_" : "bench_my_"; + + return Column( + mainAxisSize: MainAxisSize.min, + children: bench.map((playerName) { + final num = controller.playerNumbers[playerName] ?? "0"; + final int fouls = controller.playerStats[playerName]?["fls"] ?? 0; + final bool isFouledOut = fouls >= 5; + + Widget avatarUI = Container( + margin: EdgeInsets.only(bottom: 7 * context.sf), + decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 1.8 * context.sf), boxShadow: [BoxShadow(color: Colors.black45, blurRadius: 5 * context.sf, offset: Offset(0, 2.5 * context.sf))]), + child: CircleAvatar( + radius: 22 * context.sf, backgroundColor: isFouledOut ? Colors.grey.shade800 : teamColor, + child: Text(num, style: TextStyle(color: isFouledOut ? Colors.red.shade300 : Colors.white, fontSize: 16 * context.sf, fontWeight: FontWeight.bold, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), + ), + ); + + if (isFouledOut) { + return GestureDetector(onTap: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $playerName não pode voltar (Expulso).'), backgroundColor: Colors.red)), child: avatarUI); + } + + return Draggable( + data: "$prefix$playerName", + feedback: Material(color: Colors.transparent, child: CircleAvatar(radius: 28 * context.sf, backgroundColor: teamColor, child: Text(num, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18 * context.sf)))), + childWhenDragging: Opacity(opacity: 0.5, child: SizedBox(width: 45 * context.sf, height: 45 * context.sf)), + child: avatarUI, + ); + }).toList(), + ); + } +} + +class PlayerCourtCard extends StatelessWidget { + final PlacarController controller; + final String name; + final bool isOpponent; + const PlayerCourtCard({super.key, required this.controller, required this.name, required this.isOpponent}); + + @override + Widget build(BuildContext context) { + final teamColor = isOpponent ? const Color(0xFFD92C2C) : const Color(0xFF1E5BB2); + final stats = controller.playerStats[name]!; + final number = controller.playerNumbers[name]!; + final prefix = isOpponent ? "player_opp_" : "player_my_"; + + return Draggable( + data: "$prefix$name", + feedback: Material( + color: Colors.transparent, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 18 * context.sf, vertical: 11 * context.sf), + decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(9 * context.sf)), + child: Text(name, style: TextStyle(color: Colors.white, fontSize: 20 * context.sf, fontWeight: FontWeight.bold)), + ), + ), + childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(context, number, name, stats, teamColor, false, false)), + child: DragTarget( + onAcceptWithDetails: (details) { + final action = details.data; + if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) controller.handleActionDrag(context, action, "$prefix$name"); + else if (action.startsWith("bench_")) controller.handleSubbing(context, action, name, isOpponent); + }, + builder: (context, candidateData, rejectedData) { + 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(context, number, name, stats, teamColor, isSubbing, isActionHover); + }, + ), + ); + } + + Widget _playerCardUI(BuildContext context, String number, String name, Map stats, Color teamColor, bool isSubbing, bool isActionHover) { + bool isFouledOut = stats["fls"]! >= 5; + Color bgColor = isFouledOut ? Colors.red.shade50 : Colors.white; + Color borderColor = isFouledOut ? Colors.redAccent : Colors.transparent; + + if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = Colors.blue; } + else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = Colors.orange; } + + int fgm = stats["fgm"]!; int fga = stats["fga"]!; + String fgPercent = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) : "0"; + String displayName = name.length > 12 ? "${name.substring(0, 10)}..." : name; + + return Container( + decoration: BoxDecoration( + color: bgColor, borderRadius: BorderRadius.circular(11 * context.sf), + border: Border.all(color: borderColor, width: 1.8 * context.sf), + boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5 * context.sf, offset: Offset(2 * context.sf, 3.5 * context.sf))], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(9 * context.sf), + child: IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16 * context.sf), color: isFouledOut ? Colors.grey[700] : teamColor, + alignment: Alignment.center, child: Text(number, style: TextStyle(color: Colors.white, fontSize: 22 * context.sf, fontWeight: FontWeight.bold)), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12 * context.sf, vertical: 7 * context.sf), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, + children: [ + Text(displayName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: isFouledOut ? Colors.red : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), + SizedBox(height: 2.5 * context.sf), + Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 12 * context.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 * context.sf, color: isFouledOut ? Colors.red : Colors.grey[500], fontWeight: FontWeight.w600)), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +class ActionButtonsPanel extends StatelessWidget { + final PlacarController controller; + const ActionButtonsPanel({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final double baseSize = 65 * context.sf; + final double feedSize = 82 * context.sf; + final double gap = 7 * context.sf; + + return Row( + mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _columnBtn([ + _dragAndTargetBtn(context, "M1", Colors.redAccent, "miss_1", baseSize, feedSize), + _dragAndTargetBtn(context, "1", Colors.orange, "add_pts_1", baseSize, feedSize), + _dragAndTargetBtn(context, "1", Colors.orange, "sub_pts_1", baseSize, feedSize, isX: true), + _dragAndTargetBtn(context, "STL", Colors.green, "add_stl", baseSize, feedSize), + ], gap), + SizedBox(width: gap * 1), + _columnBtn([ + _dragAndTargetBtn(context, "M2", Colors.redAccent, "miss_2", baseSize, feedSize), + _dragAndTargetBtn(context, "2", Colors.orange, "add_pts_2", baseSize, feedSize), + _dragAndTargetBtn(context, "2", Colors.orange, "sub_pts_2", baseSize, feedSize, isX: true), + _dragAndTargetBtn(context, "AST", Colors.blueGrey, "add_ast", baseSize, feedSize), + ], gap), + SizedBox(width: gap * 1), + _columnBtn([ + _dragAndTargetBtn(context, "M3", Colors.redAccent, "miss_3", baseSize, feedSize), + _dragAndTargetBtn(context, "3", Colors.orange, "add_pts_3", baseSize, feedSize), + _dragAndTargetBtn(context, "3", Colors.orange, "sub_pts_3", baseSize, feedSize, isX: true), + _dragAndTargetBtn(context, "TOV", Colors.redAccent, "add_tov", baseSize, feedSize), + ], gap), + SizedBox(width: gap * 1), + _columnBtn([ + _dragAndTargetBtn(context, "ORB", const Color(0xFF1E2A38), "add_orb", baseSize, feedSize, icon: Icons.sports_basketball), + _dragAndTargetBtn(context, "DRB", const Color(0xFF1E2A38), "add_drb", baseSize, feedSize, icon: Icons.sports_basketball), + _dragAndTargetBtn(context, "BLK", Colors.deepPurple, "add_blk", baseSize, feedSize, icon: Icons.front_hand), + ], gap), + ], + ); + } + + Widget _columnBtn(List children, double gap) { + return Column(mainAxisSize: MainAxisSize.min, children: children.map((c) => Padding(padding: EdgeInsets.only(bottom: gap), child: c)).toList()); + } + + Widget _dragAndTargetBtn(BuildContext context, String label, Color color, String actionData, double baseSize, double feedSize, {IconData? icon, bool isX = false}) { + return Draggable( + data: actionData, + feedback: _circle(context, label, color, icon, true, baseSize, feedSize, isX: isX), + childWhenDragging: Opacity(opacity: 0.5, child: _circle(context, label, color, icon, false, baseSize, feedSize, isX: isX)), + child: DragTarget( + 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 * context.sf, spreadRadius: 3 * context.sf)]) : null, child: _circle(context, label, color, icon, false, baseSize, feedSize, isX: isX)), + ); + } + ), + ); + } + + Widget _circle(BuildContext context, String label, Color color, IconData? icon, bool isFeed, double baseSize, double feedSize, {bool isX = false}) { + double size = isFeed ? feedSize : baseSize; + Widget content; + bool isPointBtn = label == "1" || label == "2" || label == "3" || label == "M1" || label == "M2" || label == "M3"; + bool isBlkBtn = label == "BLK"; + + if (isPointBtn) { + content = Stack( + alignment: Alignment.center, + children: [ + Container(width: size * 0.75, height: size * 0.75, decoration: const BoxDecoration(color: Colors.black, shape: BoxShape.circle)), + Icon(Icons.sports_basketball, color: color, size: size * 0.9), + Stack( + children: [ + Text(label, style: TextStyle(fontSize: size * 0.38, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = size * 0.05..color = Colors.white, decoration: TextDecoration.none)), + Text(label, style: TextStyle(fontSize: size * 0.38, fontWeight: FontWeight.w900, color: Colors.black, decoration: TextDecoration.none)), + ], + ), + ], + ); + } else if (isBlkBtn) { + content = Stack( + alignment: Alignment.center, + children: [ + Icon(Icons.front_hand, color: const Color.fromARGB(207, 56, 52, 52), size: size * 0.75), + Stack( + alignment: Alignment.center, + children: [ + Text(label, style: TextStyle(fontSize: size * 0.28, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = size * 0.05..color = Colors.black, decoration: TextDecoration.none)), + Text(label, style: TextStyle(fontSize: size * 0.28, fontWeight: FontWeight.w900, color: Colors.white, decoration: TextDecoration.none)), + ], + ), + ], + ); + } else if (icon != null) { + content = Icon(icon, color: Colors.white, size: size * 0.5); + } else { + content = Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: size * 0.35, decoration: TextDecoration.none)); + } + + return Stack( + clipBehavior: Clip.none, alignment: Alignment.bottomRight, + 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 * context.sf, offset: Offset(0, 3 * context.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))), + ], + ); + } } \ No newline at end of file diff --git a/lib/pages/RegisterPage.dart b/lib/pages/RegisterPage.dart index 2552d95..38b3928 100644 --- a/lib/pages/RegisterPage.dart +++ b/lib/pages/RegisterPage.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../controllers/register_controller.dart'; import '../widgets/register_widgets.dart'; +import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER! class RegisterPage extends StatefulWidget { const RegisterPage({super.key}); @@ -10,39 +11,44 @@ class RegisterPage extends StatefulWidget { } class _RegisterPageState extends State { - // Instancia o controller final RegisterController _controller = RegisterController(); @override void dispose() { - _controller.dispose(); // Limpa a memória ao sair + _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text("Criar Conta")), + backgroundColor: Colors.white, + appBar: AppBar( + title: Text("Criar Conta", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), + backgroundColor: Colors.white, + elevation: 0, + ), body: Center( child: SingleChildScrollView( - padding: const EdgeInsets.all(24.0), + padding: EdgeInsets.all(24.0 * context.sf), child: ListenableBuilder( - listenable: _controller, // Ouve as mudanças (loading) + listenable: _controller, builder: (context, child) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - "Junta-te à Equipa!", - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 30), - - // Widgets Extraídos - RegisterFormFields(controller: _controller), - const SizedBox(height: 24), - RegisterButton(controller: _controller), - ], + return Container( + width: double.infinity, + constraints: BoxConstraints(maxWidth: 450 * context.sf), // Mesma largura do Login + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const RegisterHeader(), // 🔥 Agora sim, usa o Header bonito! + SizedBox(height: 30 * context.sf), + + RegisterFormFields(controller: _controller), + SizedBox(height: 24 * context.sf), + + RegisterButton(controller: _controller), + ], + ), ); }, ), diff --git a/lib/pages/gamePage.dart b/lib/pages/gamePage.dart index 37c1465..82e104d 100644 --- a/lib/pages/gamePage.dart +++ b/lib/pages/gamePage.dart @@ -3,114 +3,83 @@ import 'package:playmaker/pages/PlacarPage.dart'; import '../controllers/game_controller.dart'; import '../controllers/team_controller.dart'; import '../models/game_model.dart'; -import 'dart:math' as math; +import '../utils/size_extension.dart'; // 👇 NOVO SUPERPODER AQUI TAMBÉM! // --- CARD DE EXIBIÇÃO DO JOGO --- class GameResultCard extends StatelessWidget { - final String gameId; - final String myTeam, opponentTeam, myScore, opponentScore, status, season; - final String? myTeamLogo; - final String? opponentTeamLogo; - final double sf; + final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season; + final String? myTeamLogo, opponentTeamLogo; const GameResultCard({ - super.key, - required this.gameId, - required this.myTeam, - required this.opponentTeam, - required this.myScore, - required this.opponentScore, - required this.status, - required this.season, - this.myTeamLogo, - this.opponentTeamLogo, - 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, }); @override Widget build(BuildContext context) { return Container( - margin: EdgeInsets.only(bottom: 16 * sf), - padding: EdgeInsets.all(16 * sf), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20 * sf), - boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)], - ), + margin: EdgeInsets.only(bottom: 16 * context.sf), + padding: EdgeInsets.all(16 * context.sf), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * context.sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * context.sf)]), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, sf)), - _buildScoreCenter(context, gameId, sf), - Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, sf)), + 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) { + 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), - 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) { + Widget _buildScoreCenter(BuildContext context, String id) { return Column( children: [ Row( mainAxisSize: MainAxisSize.min, children: [ - _scoreBox(myScore, Colors.green, sf), - Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf)), - _scoreBox(opponentScore, Colors.grey, sf), + _scoreBox(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: const Color(0xFFE74C3C)), - label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)), - style: TextButton.styleFrom( - backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), - padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), - visualDensity: VisualDensity.compact, - ), + onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))), + icon: Icon(Icons.play_circle_fill, size: 18 * context.sf, color: const Color(0xFFE74C3C)), + label: Text("RETORNAR", style: TextStyle(fontSize: 11 * context.sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)), + style: TextButton.styleFrom(backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), padding: EdgeInsets.symmetric(horizontal: 14 * context.sf, vertical: 8 * context.sf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), visualDensity: VisualDensity.compact), ), - SizedBox(height: 6 * 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)), ); } // --- POPUP DE CRIAÇÃO --- class CreateGameDialogManual extends StatefulWidget { final TeamController teamController; - final GameController gameController; - final double sf; + final GameController gameController; - 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(); @@ -136,27 +105,24 @@ class _CreateGameDialogManualState extends State { @override Widget build(BuildContext context) { return AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * widget.sf)), - title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * widget.sf)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * 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), - 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), + TextField(controller: _seasonController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * context.sf))), + SizedBox(height: 15 * context.sf), + _buildSearch(context, "Minha Equipa", _myTeamController), + Padding(padding: EdgeInsets.symmetric(vertical: 10 * context.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * context.sf))), + _buildSearch(context, "Adversário", _opponentController), ], ), ), actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf))), + TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * context.sf))), ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * 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); @@ -168,13 +134,13 @@ 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)), ), ], ); } - Widget _buildSearch({required String label, required TextEditingController controller, required double sf}) { + Widget _buildSearch(BuildContext context, String label, TextEditingController controller) { return StreamBuilder>>( stream: widget.teamController.teamsStream, builder: (context, snapshot) { @@ -190,9 +156,9 @@ class _CreateGameDialogManualState extends State { return Align( alignment: Alignment.topLeft, child: Material( - elevation: 4.0, borderRadius: BorderRadius.circular(8 * sf), + elevation: 4.0, borderRadius: BorderRadius.circular(8 * context.sf), child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: 250 * 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) { @@ -200,8 +166,8 @@ class _CreateGameDialogManualState extends State { 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)), + 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); }, ); }, @@ -211,11 +177,11 @@ class _CreateGameDialogManualState extends State { ); }, fieldViewBuilder: (ctx, txtCtrl, node, submit) { - if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) { txtCtrl.text = controller.text; } + if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) txtCtrl.text = controller.text; txtCtrl.addListener(() { controller.text = txtCtrl.text; }); 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()), + 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,7 +190,7 @@ class _CreateGameDialogManualState extends State { } } -// --- PÁGINA PRINCIPAL DOS JOGOS COM FILTROS --- +// --- PÁGINA PRINCIPAL DOS JOGOS --- class GamePage extends StatefulWidget { const GamePage({super.key}); @@ -235,96 +201,54 @@ class GamePage extends StatefulWidget { class _GamePageState extends State { final GameController gameController = GameController(); final TeamController teamController = TeamController(); - - // Variáveis para os filtros String selectedSeason = 'Todas'; String selectedTeam = 'Todas'; @override Widget build(BuildContext context) { - final double wScreen = MediaQuery.of(context).size.width; - final double hScreen = MediaQuery.of(context).size.height; - final double sf = math.min(wScreen, hScreen) / 400; - - // Verifica se algum filtro está ativo para mudar a cor do ícone bool isFilterActive = selectedSeason != 'Todas' || selectedTeam != 'Todas'; return Scaffold( backgroundColor: const Color(0xFFF5F7FA), appBar: AppBar( - title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * sf)), + title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)), backgroundColor: Colors.white, elevation: 0, actions: [ - // 👇 BOTÃO DE FILTRO NA APP BAR 👇 Padding( - padding: EdgeInsets.only(right: 8.0 * sf), + padding: EdgeInsets.only(right: 8.0 * context.sf), child: IconButton( - icon: Icon( - isFilterActive ? Icons.filter_list_alt : Icons.filter_list, - color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87, - size: 26 * sf, - ), - onPressed: () => _showFilterPopup(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), ), ) ], ), - body: StreamBuilder>>( stream: teamController.teamsStream, builder: (context, teamSnapshot) { final List> teamsList = teamSnapshot.data ?? []; - return StreamBuilder>( stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason), builder: (context, gameSnapshot) { - if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) { - return const Center(child: CircularProgressIndicator()); - } - - if (gameSnapshot.hasError) { - return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * sf))); - } - + if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) return const Center(child: CircularProgressIndicator()); + if (gameSnapshot.hasError) return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * context.sf))); if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.search_off, size: 48 * sf, color: Colors.grey.shade300), - SizedBox(height: 10 * sf), - Text("Nenhum jogo encontrado para este filtro.", style: TextStyle(fontSize: 14 * sf, color: Colors.grey.shade600)), - ], - ) - ); + 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 * sf), + padding: EdgeInsets.all(16 * context.sf), itemCount: gameSnapshot.data!.length, itemBuilder: (context, index) { final game = gameSnapshot.data![index]; - - String? myLogo; - String? oppLogo; - + String? myLogo, oppLogo; for (var team in teamsList) { - if (team['name'] == game.myTeam) { myLogo = team['image_url']; } - if (team['name'] == game.opponentTeam) { oppLogo = team['image_url']; } + if (team['name'] == game.myTeam) myLogo = team['image_url']; + if (team['name'] == game.opponentTeam) oppLogo = team['image_url']; } - 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: sf, + 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, ); }, ); @@ -334,92 +258,60 @@ class _GamePageState extends State { ), floatingActionButton: FloatingActionButton( backgroundColor: const Color(0xFFE74C3C), - child: Icon(Icons.add, color: Colors.white, size: 24 * sf), - onPressed: () => _showCreateDialog(context, sf), + child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf), + onPressed: () => showDialog(context: context, builder: (context) => CreateGameDialogManual(teamController: teamController, gameController: gameController)), ), ); } - // 👇 O POPUP DE FILTROS 👇 - void _showFilterPopup(BuildContext context, double sf) { - // Variáveis temporárias para o Popup (para não atualizar a lista antes de clicar em "Aplicar") + void _showFilterPopup(BuildContext context) { String tempSeason = selectedSeason; String tempTeam = selectedTeam; - showDialog( context: context, builder: (context) { - // StatefulBuilder permite atualizar a interface APENAS dentro do Popup return StatefulBuilder( builder: (context, setPopupState) { return AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * sf)), - IconButton( - icon: const Icon(Icons.close, color: Colors.grey), - onPressed: () => Navigator.pop(context), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ) + 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, + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 1. Filtro de Temporada - Text("Temporada", style: TextStyle(fontSize: 12 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), - SizedBox(height: 6 * sf), + 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 * sf), - decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * sf)), + padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)), child: DropdownButtonHideUnderline( child: DropdownButton( - isExpanded: true, - value: tempSeason, - style: TextStyle(fontSize: 14 * sf, color: Colors.black87, fontWeight: FontWeight.bold), - items: ['Todas', '2024/25', '2025/26'].map((String value) { - return DropdownMenuItem(value: value, child: Text(value)); - }).toList(), - onChanged: (newValue) { - setPopupState(() => tempSeason = newValue!); - }, + 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 * sf), - - // 2. Filtro de Equipa - Text("Equipa", style: TextStyle(fontSize: 12 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), - SizedBox(height: 6 * sf), + SizedBox(height: 20 * context.sf), + Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), + SizedBox(height: 6 * context.sf), Container( - padding: EdgeInsets.symmetric(horizontal: 12 * sf), - decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * sf)), + 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) { List teamNames = ['Todas']; - if (snapshot.hasData) { - teamNames.addAll(snapshot.data!.map((t) => t['name'].toString())); - } - + if (snapshot.hasData) teamNames.addAll(snapshot.data!.map((t) => t['name'].toString())); if (!teamNames.contains(tempTeam)) tempTeam = 'Todas'; - return DropdownButtonHideUnderline( child: DropdownButton( - isExpanded: true, - value: tempTeam, - style: TextStyle(fontSize: 14 * sf, color: Colors.black87, fontWeight: FontWeight.bold), - items: teamNames.map((String value) { - return DropdownMenuItem(value: value, child: Text(value, overflow: TextOverflow.ellipsis)); - }).toList(), - onChanged: (newValue) { - setPopupState(() => tempTeam = newValue!); - }, + 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!), ), ); } @@ -428,32 +320,8 @@ class _GamePageState extends State { ], ), actions: [ - TextButton( - onPressed: () { - // Limpar Filtros - setState(() { - selectedSeason = 'Todas'; - selectedTeam = 'Todas'; - }); - Navigator.pop(context); - }, - child: Text('LIMPAR', style: TextStyle(fontSize: 12 * sf, color: Colors.grey)) - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFE74C3C), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * sf)), - ), - onPressed: () { - // Aplicar Filtros (atualiza a página principal) - setState(() { - selectedSeason = tempSeason; - selectedTeam = tempTeam; - }); - Navigator.pop(context); - }, - child: Text('APLICAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * sf)), - ), + TextButton(onPressed: () { setState(() { selectedSeason = 'Todas'; selectedTeam = 'Todas'; }); Navigator.pop(context); }, child: Text('LIMPAR', style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey))), + ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), onPressed: () { setState(() { selectedSeason = tempSeason; selectedTeam = tempTeam; }); Navigator.pop(context); }, child: Text('APLICAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * context.sf))), ], ); } @@ -461,15 +329,4 @@ class _GamePageState extends State { }, ); } - - void _showCreateDialog(BuildContext context, double sf) { - showDialog( - context: context, - builder: (context) => CreateGameDialogManual( - teamController: teamController, - gameController: gameController, - sf: sf, - ), - ); - } } \ No newline at end of file diff --git a/lib/pages/home.dart b/lib/pages/home.dart index bbb9390..43b7bf6 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -6,8 +6,7 @@ import 'package:playmaker/pages/teamPage.dart'; import 'package:playmaker/controllers/team_controller.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:playmaker/pages/status_page.dart'; -import 'dart:math' as math; - +import '../utils/size_extension.dart'; import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart'; class HomeScreen extends StatefulWidget { @@ -31,12 +30,10 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { - final double wScreen = MediaQuery.of(context).size.width; - final double hScreen = MediaQuery.of(context).size.height; - final double sf = math.min(wScreen, hScreen) / 400; + // Já não precisamos calcular o sf aqui! final List pages = [ - _buildHomeContent(sf, wScreen), + _buildHomeContent(context), // Passamos só o context const GamePage(), const TeamsPage(), const StatusPage(), @@ -45,11 +42,11 @@ class _HomeScreenState extends State { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( - title: Text('PlayMaker', style: TextStyle(fontSize: 20 * sf)), + title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)), backgroundColor: HomeConfig.primaryColor, foregroundColor: Colors.white, leading: IconButton( - icon: Icon(Icons.person, size: 24 * sf), + icon: Icon(Icons.person, size: 24 * context.sf), onPressed: () {}, ), ), @@ -65,7 +62,8 @@ class _HomeScreenState extends State { backgroundColor: Theme.of(context).colorScheme.surface, surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, elevation: 1, - height: 70 * math.min(sf, 1.2), + // O math.min não é necessário se já tens o sf. Mas podes usar context.sf + height: 70 * (context.sf < 1.2 ? context.sf : 1.2), destinations: const [ NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'), NavigationDestination(icon: Icon(Icons.sports_soccer_outlined), selectedIcon: Icon(Icons.sports_soccer), label: 'Jogo'), @@ -76,16 +74,16 @@ class _HomeScreenState extends State { ); } - void _showTeamSelector(BuildContext context, double sf) { + void _showTeamSelector(BuildContext context) { showModalBottomSheet( context: context, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * sf))), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf))), builder: (context) { return StreamBuilder>>( stream: _teamController.teamsStream, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); - if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * sf, child: const Center(child: Text("Nenhuma equipa criada."))); + if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * context.sf, child: const Center(child: Text("Nenhuma equipa criada."))); final teams = snapshot.data!; return ListView.builder( @@ -114,8 +112,9 @@ class _HomeScreenState extends State { ); } - Widget _buildHomeContent(double sf, double wScreen) { - final double cardHeight = (wScreen / 2) * 1.0; + Widget _buildHomeContent(BuildContext context) { + final double wScreen = MediaQuery.of(context).size.width; + final double cardHeight = wScreen * 0.5; return StreamBuilder>>( stream: _selectedTeamId != null @@ -126,44 +125,44 @@ class _HomeScreenState extends State { return SingleChildScrollView( child: Padding( - padding: EdgeInsets.symmetric(horizontal: 22.0 * sf, vertical: 16.0 * sf), + padding: EdgeInsets.symmetric(horizontal: 22.0 * context.sf, vertical: 16.0 * context.sf), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( - onTap: () => _showTeamSelector(context, sf), + onTap: () => _showTeamSelector(context), child: Container( - padding: EdgeInsets.all(12 * sf), - decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * sf), border: Border.all(color: Colors.grey.shade300)), + padding: EdgeInsets.all(12 * context.sf), + decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.shade300)), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row(children: [Icon(Icons.shield, color: HomeConfig.primaryColor, size: 24 * sf), SizedBox(width: 10 * sf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold))]), + Row(children: [Icon(Icons.shield, color: HomeConfig.primaryColor, size: 24 * context.sf), SizedBox(width: 10 * context.sf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold))]), const Icon(Icons.arrow_drop_down), ], ), ), ), - SizedBox(height: 20 * sf), + SizedBox(height: 20 * context.sf), SizedBox( height: cardHeight, child: Row( children: [ - Expanded(child: _buildStatCard(title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)), - SizedBox(width: 12 * sf), - Expanded(child: _buildStatCard(title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))), + Expanded(child: _buildStatCard(context: context, title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)), + SizedBox(width: 12 * context.sf), + Expanded(child: _buildStatCard(context: context, title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))), ], ), ), - SizedBox(height: 12 * sf), + SizedBox(height: 12 * context.sf), SizedBox( height: cardHeight, child: Row( children: [ - Expanded(child: _buildStatCard(title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))), - SizedBox(width: 12 * sf), + Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))), + SizedBox(width: 12 * context.sf), Expanded( child: PieChartCard( victories: _teamWins, @@ -172,58 +171,42 @@ class _HomeScreenState extends State { title: 'DESEMPENHO', subtitle: 'Temporada', backgroundColor: const Color(0xFFC62828), - sf: sf + sf: context.sf // Aqui o PieChartCard ainda usa sf, então passamos ), ), ], ), ), - SizedBox(height: 40 * sf), + SizedBox(height: 40 * context.sf), - Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * sf, fontWeight: FontWeight.bold, color: Colors.grey[800])), - SizedBox(height: 16 * sf), + Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[800])), + SizedBox(height: 16 * context.sf), - // 👇 HISTÓRICO LIGADO À BASE DE DADOS (COM AS COLUNAS CORRIGIDAS) 👇 -// 👇 LIGAÇÃO CORRIGIDA: Agora usa a coluna 'nome' como pediste 👇 -// 👇 HISTÓRICO DINÂMICO (Qualquer equipa) 👇 _selectedTeamName == "Selecionar Equipa" ? Container( - padding: EdgeInsets.all(20 * sf), + padding: EdgeInsets.all(20 * context.sf), alignment: Alignment.center, - child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf)), + child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)), ) : StreamBuilder>>( - // Pede os jogos ordenados pela data (sem filtros rígidos aqui) stream: _supabase.from('games').stream(primaryKey: ['id']) .order('game_date', ascending: false), builder: (context, gameSnapshot) { - if (gameSnapshot.hasError) { - return Text("Erro ao carregar jogos: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red)); - } + if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red)); + if (gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator()); - if (gameSnapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - // 👇 O CÉREBRO DA APP: Filtro inteligente no Flutter 👇 final todosOsJogos = gameSnapshot.data ?? []; - final gamesList = todosOsJogos.where((game) { String myT = game['my_team']?.toString() ?? ''; String oppT = game['opponent_team']?.toString() ?? ''; String status = game['status']?.toString() ?? ''; - - // O jogo tem de envolver a equipa selecionada E estar Terminado - bool isPlaying = (myT == _selectedTeamName || oppT == _selectedTeamName); - bool isFinished = status == 'Terminado'; - - return isPlaying && isFinished; - }).take(3).toList(); // Pega apenas nos 3 mais recentes + return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado'; + }).take(3).toList(); if (gamesList.isEmpty) { return Container( - padding: EdgeInsets.all(20 * sf), + padding: EdgeInsets.all(20 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)), alignment: Alignment.center, child: Text("Ainda não há jogos terminados para $_selectedTeamName.", style: TextStyle(color: Colors.grey)), @@ -232,47 +215,33 @@ class _HomeScreenState extends State { return Column( children: gamesList.map((game) { - - // Lê os dados brutos da base de dados String dbMyTeam = game['my_team']?.toString() ?? ''; String dbOppTeam = game['opponent_team']?.toString() ?? ''; int dbMyScore = int.tryParse(game['my_score'].toString()) ?? 0; int dbOppScore = int.tryParse(game['opponent_score'].toString()) ?? 0; - String opponent; - int myScore; - int oppScore; + String opponent; int myScore; int oppScore; - // 🔄 MAGIA DA INVERSÃO DE RESULTADOS 🔄 - // Garante que os pontos da equipa selecionada aparecem sempre do lado esquerdo if (dbMyTeam == _selectedTeamName) { - // A equipa que escolhemos está guardada no 'my_team' - opponent = dbOppTeam; - myScore = dbMyScore; - oppScore = dbOppScore; + opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore; } else { - // A equipa que escolhemos está guardada no 'opponent_team' - opponent = dbMyTeam; - myScore = dbOppScore; - oppScore = dbMyScore; + opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore; } - // Limpa a data (Remove as horas e deixa só YYYY-MM-DD) String rawDate = game['game_date']?.toString() ?? '---'; String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate; - // Calcula Vitória, Empate ou Derrota para a equipa selecionada String result = 'E'; if (myScore > oppScore) result = 'V'; if (myScore < oppScore) result = 'D'; return _buildGameHistoryCard( + context: context, // Usamos o context para o sf opponent: opponent, result: result, myScore: myScore, oppScore: oppScore, date: date, - sf: sf, topPts: game['top_pts_name'] ?? '---', topAst: game['top_ast_name'] ?? '---', topRbs: game['top_rbs_name'] ?? '---', @@ -284,7 +253,7 @@ class _HomeScreenState extends State { }, ), - SizedBox(height: 20 * sf), + SizedBox(height: 20 * context.sf), ], ), ), @@ -308,7 +277,7 @@ class _HomeScreenState extends State { return {'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap), 'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap), 'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)}; } - Widget _buildStatCard({required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) { + Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) { return Card( elevation: 4, margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none), @@ -329,8 +298,7 @@ class _HomeScreenState extends State { SizedBox( width: double.infinity, child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, + fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)), ), ), @@ -340,8 +308,7 @@ class _HomeScreenState extends State { Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))), const Spacer(), Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: ch * 0.035), + width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035), decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)), child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold))) ), @@ -355,7 +322,7 @@ class _HomeScreenState extends State { } Widget _buildGameHistoryCard({ - required String opponent, required String result, required int myScore, required int oppScore, required String date, required double sf, + required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date, required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp }) { bool isWin = result == 'V'; @@ -363,44 +330,42 @@ class _HomeScreenState extends State { Color statusColor = isWin ? Colors.green : (isDraw ? Colors.yellow.shade700 : Colors.red); return Container( - margin: EdgeInsets.only(bottom: 14 * sf), + margin: EdgeInsets.only(bottom: 14 * context.sf), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.shade200), - boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))], + color: Colors.white, borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))], ), child: Column( children: [ Padding( - padding: EdgeInsets.all(14 * sf), + padding: EdgeInsets.all(14 * context.sf), child: Row( children: [ Container( - width: 36 * sf, height: 36 * sf, + width: 36 * context.sf, height: 36 * context.sf, decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle), - child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * sf))), + child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf))), ), - SizedBox(width: 14 * sf), + SizedBox(width: 14 * context.sf), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(date, style: TextStyle(fontSize: 11 * sf, color: Colors.grey, fontWeight: FontWeight.w600)), - SizedBox(height: 6 * sf), + Text(date, style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.w600)), + SizedBox(height: 6 * context.sf), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)), + Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)), Padding( - padding: EdgeInsets.symmetric(horizontal: 8 * sf), + padding: EdgeInsets.symmetric(horizontal: 8 * context.sf), child: Container( - padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 4 * sf), + padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8)), - child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)), + child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)), ), ), - Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)), + Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)), ], ), ], @@ -409,35 +374,29 @@ class _HomeScreenState extends State { ], ), ), - Divider(height: 1, color: Colors.grey.shade100, thickness: 1.5), - Container( - width: double.infinity, - padding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 12 * sf), - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16)), - ), + width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf), + decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))), child: Column( children: [ Row( children: [ - Expanded(child: _buildGridStatRow(Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, sf, isMvp: true)), - Expanded(child: _buildGridStatRow(Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef, sf)), + Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)), + Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef)), ], ), - SizedBox(height: 8 * sf), + SizedBox(height: 8 * context.sf), Row( children: [ - Expanded(child: _buildGridStatRow(Icons.bolt, Colors.blue.shade700, "Pontos", topPts, sf)), - Expanded(child: _buildGridStatRow(Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs, sf)), + Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)), + Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs)), ], ), - SizedBox(height: 8 * sf), + SizedBox(height: 8 * context.sf), Row( children: [ - Expanded(child: _buildGridStatRow(Icons.star, Colors.green.shade700, "Assists", topAst, sf)), + Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)), const Expanded(child: SizedBox()), ], ), @@ -449,22 +408,21 @@ class _HomeScreenState extends State { ); } - Widget _buildGridStatRow(IconData icon, Color color, String label, String value, double sf, {bool isMvp = false}) { + Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, String label, String value, {bool isMvp = false}) { return Row( children: [ - Icon(icon, size: 14 * sf, color: color), - SizedBox(width: 4 * sf), - Text('$label: ', style: TextStyle(fontSize: 11 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), + Icon(icon, size: 14 * context.sf, color: color), + SizedBox(width: 4 * context.sf), + Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), Expanded( child: Text( value, style: TextStyle( - fontSize: 11 * sf, + fontSize: 11 * context.sf, color: isMvp ? Colors.amber.shade900 : Colors.black87, fontWeight: FontWeight.bold ), - maxLines: 1, - overflow: TextOverflow.ellipsis + maxLines: 1, overflow: TextOverflow.ellipsis ) ), ], diff --git a/lib/pages/login.dart b/lib/pages/login.dart index 93839b3..036f3f1 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:playmaker/controllers/login_controller.dart'; import '../widgets/login_widgets.dart'; import 'home.dart'; // <--- IMPORTANTE: Importa a tua HomeScreen +import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER! class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -27,47 +28,40 @@ class _LoginPageState extends State { child: ListenableBuilder( listenable: controller, builder: (context, child) { - return LayoutBuilder( - builder: (context, constraints) { - final screenWidth = constraints.maxWidth; - - return Center( - child: SingleChildScrollView( - child: Container( - width: screenWidth * 0.6, - constraints: const BoxConstraints(minWidth: 340), - padding: const EdgeInsets.all(32), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const BasketTrackHeader(), - const SizedBox(height: 40), - - LoginFormFields(controller: controller), - const SizedBox(height: 24), - - // AQUI ESTÁ A MUDANÇA PRINCIPAL - LoginButton( - controller: controller, - onLoginSuccess: () { - // Verifica se o widget ainda está no ecrã antes de navegar - if (mounted) { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); - } - }, - ), - const SizedBox(height: 16), - - const CreateAccountButton(), - ], + return Center( + child: SingleChildScrollView( + child: Container( + width: double.infinity, + // Garante que o form não fica gigante num tablet + constraints: BoxConstraints(maxWidth: 450 * context.sf), + padding: EdgeInsets.all(32 * context.sf), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const BasketTrackHeader(), + SizedBox(height: 40 * context.sf), + + LoginFormFields(controller: controller), + SizedBox(height: 24 * context.sf), + + LoginButton( + controller: controller, + onLoginSuccess: () { + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const HomeScreen()), + ); + } + }, ), - ), + SizedBox(height: 16 * context.sf), + + const CreateAccountButton(), + ], ), - ); - }, + ), + ), ); }, ), diff --git a/lib/pages/status_page.dart b/lib/pages/status_page.dart index 1a0cf6d..8d3b380 100644 --- a/lib/pages/status_page.dart +++ b/lib/pages/status_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../controllers/team_controller.dart'; -import 'dart:math' as math; +import '../utils/size_extension.dart'; // 👇 A MAGIA DO SF! class StatusPage extends StatefulWidget { const StatusPage({super.key}); @@ -21,20 +21,17 @@ class _StatusPageState extends State { @override Widget build(BuildContext context) { - final double sf = math.min(MediaQuery.of(context).size.width, MediaQuery.of(context).size.height) / 400; - return Column( children: [ - // --- SELETOR DE EQUIPA --- Padding( - padding: EdgeInsets.all(16.0 * sf), + padding: EdgeInsets.all(16.0 * context.sf), child: InkWell( - onTap: () => _showTeamSelector(context, sf), + onTap: () => _showTeamSelector(context), child: Container( - padding: EdgeInsets.all(12 * sf), + padding: EdgeInsets.all(12 * context.sf), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(15 * sf), + borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.shade300), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)] ), @@ -42,9 +39,9 @@ class _StatusPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row(children: [ - Icon(Icons.shield, color: const Color(0xFFE74C3C), size: 24 * sf), - SizedBox(width: 10 * sf), - Text(_selectedTeamName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold)) + Icon(Icons.shield, color: const Color(0xFFE74C3C), size: 24 * context.sf), + SizedBox(width: 10 * context.sf), + Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold)) ]), const Icon(Icons.arrow_drop_down), ], @@ -53,60 +50,39 @@ class _StatusPageState extends State { ), ), - // --- TABELA DE ESTATÍSTICAS (AGORA EM TEMPO REAL) --- Expanded( child: _selectedTeamId == null - ? Center(child: Text("Seleciona uma equipa acima.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf))) - - // 👇 STREAM 1: LÊ AS ESTATÍSTICAS 👇 + ? Center(child: Text("Seleciona uma equipa acima.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf))) : StreamBuilder>>( stream: _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!), builder: (context, statsSnapshot) { - - // 👇 STREAM 2: LÊ OS JOGOS (Para os MVPs e contagem de jogos da equipa) 👇 return StreamBuilder>>( stream: _supabase.from('games').stream(primaryKey: ['id']).eq('my_team', _selectedTeamName), builder: (context, gamesSnapshot) { - - // 👇 STREAM 3: LÊ TODOS OS MEMBROS DO PLANTEL 👇 - // 👇 A CORREÇÃO ESTÁ AQUI: Remover o .eq('type', 'Jogador') -return StreamBuilder>>( - stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!), - builder: (context, membersSnapshot) { - - // Verifica se ALGUM dos 3 streams ainda está a carregar - if (statsSnapshot.connectionState == ConnectionState.waiting || - gamesSnapshot.connectionState == ConnectionState.waiting || - membersSnapshot.connectionState == ConnectionState.waiting) { + return StreamBuilder>>( + stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!), + builder: (context, membersSnapshot) { + if (statsSnapshot.connectionState == ConnectionState.waiting || gamesSnapshot.connectionState == ConnectionState.waiting || membersSnapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator(color: Color(0xFFE74C3C))); } final membersData = membersSnapshot.data ?? []; - - if (membersData.isEmpty) { - return Center(child: Text("Esta equipa não tem jogadores registados.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf))); - } + if (membersData.isEmpty) return Center(child: Text("Esta equipa não tem jogadores registados.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf))); final statsData = statsSnapshot.data ?? []; final gamesData = gamesSnapshot.data ?? []; - - // Conta o total de jogos terminados da equipa final totalGamesPlayedByTeam = gamesData.where((g) => g['status'] == 'Terminado').length; - // Agrega os dados final List> playerTotals = _aggregateStats(statsData, gamesData, membersData); - - // Calcula os Totais da Equipa final teamTotals = _calculateTeamTotals(playerTotals, totalGamesPlayedByTeam); - // Ordenação playerTotals.sort((a, b) { var valA = a[_sortColumn] ?? 0; var valB = b[_sortColumn] ?? 0; return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA); }); - return _buildStatsGrid(playerTotals, teamTotals, sf); + return _buildStatsGrid(context, playerTotals, teamTotals); } ); } @@ -118,29 +94,17 @@ return StreamBuilder>>( ); } - // --- CÉREBRO CORRIGIDO --- List> _aggregateStats(List stats, List games, List members) { Map> aggregated = {}; - // 1. Mete a malta toda do plantel com ZERO JOGOS e ZERO STATS for (var member in members) { String name = member['name']?.toString() ?? "Desconhecido"; - aggregated[name] = { - 'name': name, - 'j': 0, - 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0, - }; + aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0}; } - // 2. Se o jogador tiver linha nas estatísticas, soma +1 Jogo e os pontos dele for (var row in stats) { String name = row['player_name']?.toString() ?? "Desconhecido"; - - if (!aggregated.containsKey(name)) { - aggregated[name] = { - 'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0, - }; - } + if (!aggregated.containsKey(name)) aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0}; aggregated[name]!['j'] += 1; aggregated[name]!['pts'] += (row['pts'] ?? 0); @@ -150,20 +114,13 @@ return StreamBuilder>>( aggregated[name]!['blk'] += (row['blk'] ?? 0); } - // 3. Conta os troféus for (var game in games) { String? mvp = game['mvp_name']; String? defRaw = game['top_def_name']; - - if (mvp != null && aggregated.containsKey(mvp)) { - aggregated[mvp]!['mvp'] += 1; - } - + if (mvp != null && aggregated.containsKey(mvp)) aggregated[mvp]!['mvp'] += 1; if (defRaw != null) { String defName = defRaw.split(' (')[0].trim(); - if (aggregated.containsKey(defName)) { - aggregated[defName]!['def'] += 1; - } + if (aggregated.containsKey(defName)) aggregated[defName]!['def'] += 1; } } return aggregated.values.toList(); @@ -172,14 +129,12 @@ return StreamBuilder>>( Map _calculateTeamTotals(List> players, int teamGames) { int tPts = 0, tAst = 0, tRbs = 0, tStl = 0, tBlk = 0, tMvp = 0, tDef = 0; for (var p in players) { - tPts += (p['pts'] as int); tAst += (p['ast'] as int); tRbs += (p['rbs'] as int); - tStl += (p['stl'] as int); tBlk += (p['blk'] as int); tMvp += (p['mvp'] as int); - tDef += (p['def'] as int); + tPts += (p['pts'] as int); tAst += (p['ast'] as int); tRbs += (p['rbs'] as int); tStl += (p['stl'] as int); tBlk += (p['blk'] as int); tMvp += (p['mvp'] as int); tDef += (p['def'] as int); } return {'name': 'TOTAL EQUIPA', 'j': teamGames, 'pts': tPts, 'ast': tAst, 'rbs': tRbs, 'stl': tStl, 'blk': tBlk, 'mvp': tMvp, 'def': tDef}; } - Widget _buildStatsGrid(List> players, Map teamTotals, double sf) { + Widget _buildStatsGrid(BuildContext context, List> players, Map teamTotals) { return Container( color: Colors.white, child: SingleChildScrollView( @@ -187,49 +142,44 @@ return StreamBuilder>>( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: DataTable( - columnSpacing: 25 * sf, + columnSpacing: 25 * context.sf, headingRowColor: MaterialStateProperty.all(Colors.grey.shade100), - dataRowHeight: 60 * sf, + dataRowHeight: 60 * context.sf, columns: [ DataColumn(label: const Text('JOGADOR')), - _buildSortableColumn('J', 'j', sf), - _buildSortableColumn('PTS', 'pts', sf), - _buildSortableColumn('AST', 'ast', sf), - _buildSortableColumn('RBS', 'rbs', sf), - _buildSortableColumn('STL', 'stl', sf), - _buildSortableColumn('BLK', 'blk', sf), - _buildSortableColumn('DEF 🛡️', 'def', sf), - _buildSortableColumn('MVP 🏆', 'mvp', sf), + _buildSortableColumn(context, 'J', 'j'), + _buildSortableColumn(context, 'PTS', 'pts'), + _buildSortableColumn(context, 'AST', 'ast'), + _buildSortableColumn(context, 'RBS', 'rbs'), + _buildSortableColumn(context, 'STL', 'stl'), + _buildSortableColumn(context, 'BLK', 'blk'), + _buildSortableColumn(context, 'DEF 🛡️', 'def'), + _buildSortableColumn(context, 'MVP 🏆', 'mvp'), ], rows: [ ...players.map((player) => DataRow(cells: [ - DataCell(Row(children: [ - CircleAvatar(radius: 15 * sf, backgroundColor: Colors.grey.shade200, child: Icon(Icons.person, size: 18 * sf)), - SizedBox(width: 10 * sf), - Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf)), - ])), + DataCell(Row(children: [CircleAvatar(radius: 15 * context.sf, backgroundColor: Colors.grey.shade200, child: Icon(Icons.person, size: 18 * context.sf)), SizedBox(width: 10 * context.sf), Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf))])), DataCell(Center(child: Text(player['j'].toString()))), - _buildStatCell(player['pts'], sf, isHighlight: true), - _buildStatCell(player['ast'], sf), - _buildStatCell(player['rbs'], sf), - _buildStatCell(player['stl'], sf), - _buildStatCell(player['blk'], sf), - _buildStatCell(player['def'], sf, isBlue: true), - _buildStatCell(player['mvp'], sf, isGold: true), + _buildStatCell(context, player['pts'], isHighlight: true), + _buildStatCell(context, player['ast']), + _buildStatCell(context, player['rbs']), + _buildStatCell(context, player['stl']), + _buildStatCell(context, player['blk']), + _buildStatCell(context, player['def'], isBlue: true), + _buildStatCell(context, player['mvp'], isGold: true), ])), - DataRow( color: MaterialStateProperty.all(Colors.grey.shade50), cells: [ - DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.black, fontSize: 12 * sf))), + DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.black, fontSize: 12 * context.sf))), DataCell(Center(child: Text(teamTotals['j'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)))), - _buildStatCell(teamTotals['pts'], sf, isHighlight: true), - _buildStatCell(teamTotals['ast'], sf), - _buildStatCell(teamTotals['rbs'], sf), - _buildStatCell(teamTotals['stl'], sf), - _buildStatCell(teamTotals['blk'], sf), - _buildStatCell(teamTotals['def'], sf, isBlue: true), - _buildStatCell(teamTotals['mvp'], sf, isGold: true), + _buildStatCell(context, teamTotals['pts'], isHighlight: true), + _buildStatCell(context, teamTotals['ast']), + _buildStatCell(context, teamTotals['rbs']), + _buildStatCell(context, teamTotals['stl']), + _buildStatCell(context, teamTotals['blk']), + _buildStatCell(context, teamTotals['def'], isBlue: true), + _buildStatCell(context, teamTotals['mvp'], isGold: true), ] ) ], @@ -239,35 +189,31 @@ return StreamBuilder>>( ); } - DataColumn _buildSortableColumn(String title, String sortKey, double sf) { + DataColumn _buildSortableColumn(BuildContext context, String title, String sortKey) { return DataColumn(label: InkWell( onTap: () => setState(() { if (_sortColumn == sortKey) _isAscending = !_isAscending; else { _sortColumn = sortKey; _isAscending = false; } }), child: Row(children: [ - Text(title, style: TextStyle(fontSize: 12 * sf, fontWeight: FontWeight.bold)), - if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * sf, color: const Color(0xFFE74C3C)), + Text(title, style: TextStyle(fontSize: 12 * context.sf, fontWeight: FontWeight.bold)), + if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * context.sf, color: const Color(0xFFE74C3C)), ]), )); } - DataCell _buildStatCell(int value, double sf, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) { + DataCell _buildStatCell(BuildContext context, int value, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) { return DataCell(Center(child: Container( - padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 4 * sf), - decoration: BoxDecoration( - color: isGold && value > 0 ? Colors.amber.withOpacity(0.2) : (isBlue && value > 0 ? Colors.blue.withOpacity(0.1) : Colors.transparent), - borderRadius: BorderRadius.circular(6), - ), + padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), + decoration: BoxDecoration(color: isGold && value > 0 ? Colors.amber.withOpacity(0.2) : (isBlue && value > 0 ? Colors.blue.withOpacity(0.1) : Colors.transparent), borderRadius: BorderRadius.circular(6)), child: Text(value == 0 ? "-" : value.toString(), style: TextStyle( fontWeight: (isHighlight || isGold || isBlue) ? FontWeight.w900 : FontWeight.w600, - fontSize: 14 * sf, - color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? Colors.green.shade700 : Colors.black87)) + fontSize: 14 * context.sf, color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? Colors.green.shade700 : Colors.black87)) )), ))); } - void _showTeamSelector(BuildContext context, double sf) { + void _showTeamSelector(BuildContext context) { showModalBottomSheet(context: context, builder: (context) => StreamBuilder>>( stream: _teamController.teamsStream, builder: (context, snapshot) { diff --git a/lib/pages/teamPage.dart b/lib/pages/teamPage.dart index 7940b1d..332dac1 100644 --- a/lib/pages/teamPage.dart +++ b/lib/pages/teamPage.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:playmaker/screens/team_stats_page.dart'; import '../controllers/team_controller.dart'; import '../models/team_model.dart'; -import 'dart:math' as math; // <-- IMPORTANTE: Adicionar para o cálculo +import '../utils/size_extension.dart'; // 👇 IMPORTANTE: O TEU NOVO SUPERPODER class TeamsPage extends StatefulWidget { const TeamsPage({super.key}); @@ -25,8 +25,7 @@ class _TeamsPageState extends State { super.dispose(); } - // --- POPUP DE FILTROS --- - void _showFilterDialog(BuildContext context, double sf) { + void _showFilterDialog(BuildContext context) { showDialog( context: context, builder: (context) { @@ -34,16 +33,13 @@ class _TeamsPageState extends State { builder: (context, setModalState) { return AlertDialog( backgroundColor: const Color(0xFF2C3E50), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Filtros de pesquisa", - style: TextStyle(color: Colors.white, fontSize: 18 * 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.white, size: 20 * sf), + icon: Icon(Icons.close, color: Colors.white, size: 20 * context.sf), onPressed: () => Navigator.pop(context), ) ], @@ -52,31 +48,27 @@ class _TeamsPageState extends State { mainAxisSize: MainAxisSize.min, children: [ const Divider(color: Colors.white24), - SizedBox(height: 16 * sf), + SizedBox(height: 16 * context.sf), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Coluna Temporada Expanded( child: _buildPopupColumn( title: "TEMPORADA", options: ['Todas', '2023/24', '2024/25', '2025/26'], currentValue: _selectedSeason, - sf: sf, onSelect: (val) { setState(() => _selectedSeason = val); setModalState(() {}); }, ), ), - SizedBox(width: 20 * sf), - // Coluna Ordenar + SizedBox(width: 20 * context.sf), Expanded( child: _buildPopupColumn( title: "ORDENAR POR", options: ['Recentes', 'Nome', 'Tamanho'], currentValue: _currentSort, - sf: sf, onSelect: (val) { setState(() => _currentSort = val); setModalState(() {}); @@ -90,7 +82,7 @@ class _TeamsPageState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text("CONCLUÍDO", style: TextStyle(color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold, fontSize: 14 * sf)), + child: Text("CONCLUÍDO", style: TextStyle(color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), ), ], ); @@ -100,30 +92,24 @@ class _TeamsPageState extends State { ); } - Widget _buildPopupColumn({ - required String title, - required List options, - required String currentValue, - required double sf, - 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 * sf, fontWeight: FontWeight.bold)), - SizedBox(height: 12 * sf), + Text(title, style: TextStyle(color: Colors.grey, fontSize: 11 * context.sf, fontWeight: FontWeight.bold)), + SizedBox(height: 12 * context.sf), ...options.map((opt) { final isSelected = currentValue == opt; return InkWell( onTap: () => onSelect(opt), child: Padding( - padding: EdgeInsets.symmetric(vertical: 8.0 * sf), + padding: EdgeInsets.symmetric(vertical: 8.0 * context.sf), child: Text( opt, style: TextStyle( color: isSelected ? const Color(0xFFE74C3C) : Colors.white70, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - fontSize: 14 * sf, + fontSize: 14 * context.sf, ), ), ), @@ -135,109 +121,84 @@ class _TeamsPageState extends State { @override Widget build(BuildContext context) { - // 👇 CÁLCULO DA ESCALA (sf) PARA SE ADAPTAR A QUALQUER ECRÃ 👇 - final double wScreen = MediaQuery.of(context).size.width; - final double hScreen = MediaQuery.of(context).size.height; - final double sf = math.min(wScreen, hScreen) / 400; - + // 🔥 OLHA QUE LIMPEZA: Já não precisamos de calcular nada aqui! return Scaffold( backgroundColor: const Color(0xFFF5F7FA), appBar: AppBar( - title: Text("Minhas Equipas", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * sf)), + title: Text("Minhas Equipas", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)), backgroundColor: const Color(0xFFF5F7FA), elevation: 0, actions: [ IconButton( - icon: Icon(Icons.filter_list, color: const Color(0xFFE74C3C), size: 24 * sf), - onPressed: () => _showFilterDialog(context, sf), + icon: Icon(Icons.filter_list, color: const Color(0xFFE74C3C), size: 24 * context.sf), + onPressed: () => _showFilterDialog(context), ), ], ), body: Column( children: [ - _buildSearchBar(sf), - Expanded(child: _buildTeamsList(sf)), + _buildSearchBar(), + Expanded(child: _buildTeamsList()), ], ), floatingActionButton: FloatingActionButton( backgroundColor: const Color(0xFFE74C3C), - child: Icon(Icons.add, color: Colors.white, size: 24 * sf), - onPressed: () => _showCreateDialog(context, sf), + child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf), + onPressed: () => _showCreateDialog(context), ), ); } - Widget _buildSearchBar(double sf) { + Widget _buildSearchBar() { return Padding( - padding: EdgeInsets.all(16.0 * sf), + padding: EdgeInsets.all(16.0 * context.sf), child: TextField( controller: _searchController, onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()), - style: TextStyle(fontSize: 16 * sf), + style: TextStyle(fontSize: 16 * context.sf), decoration: InputDecoration( hintText: 'Pesquisar equipa...', - hintStyle: TextStyle(fontSize: 16 * sf), - prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * sf), + hintStyle: TextStyle(fontSize: 16 * context.sf), + prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * context.sf), filled: true, fillColor: Colors.white, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * sf), borderSide: BorderSide.none), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * context.sf), borderSide: BorderSide.none), ), ), ); } - Widget _buildTeamsList(double sf) { + Widget _buildTeamsList() { return StreamBuilder>>( stream: controller.teamsStream, builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * sf))); - } + 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!); - // --- 1. FILTROS --- - if (_selectedSeason != 'Todas') { - data = data.where((t) => t['season'] == _selectedSeason).toList(); - } - if (_searchQuery.isNotEmpty) { - data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList(); - } + if (_selectedSeason != 'Todas') data = data.where((t) => t['season'] == _selectedSeason).toList(); + if (_searchQuery.isNotEmpty) data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList(); - // --- 2. ORDENAÇÃO --- data.sort((a, b) { 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()); - } + if (_currentSort == 'Nome') return a['name'].toString().compareTo(b['name'].toString()); + else return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString()); }); return ListView.builder( - padding: EdgeInsets.symmetric(horizontal: 16 * sf), + padding: EdgeInsets.symmetric(horizontal: 16 * context.sf), itemCount: data.length, itemBuilder: (context, index) { final team = Team.fromMap(data[index]); - 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, - sf: sf, // Passar a escala para o Card onFavoriteTap: () => controller.toggleFavorite(team.id, team.isFavorite), ), ); @@ -247,14 +208,8 @@ class _TeamsPageState extends State { ); } - void _showCreateDialog(BuildContext context, double sf) { - showDialog( - context: context, - builder: (context) => CreateTeamDialog( - sf: sf, - onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl), - ), - ); + void _showCreateDialog(BuildContext context) { + showDialog(context: context, builder: (context) => CreateTeamDialog(onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl))); } } @@ -263,129 +218,58 @@ class TeamCard extends StatelessWidget { final Team team; final TeamController controller; final VoidCallback onFavoriteTap; - final double sf; // <-- Variável de escala - const TeamCard({ - super.key, - required this.team, - required this.controller, - required this.onFavoriteTap, - required this.sf, - }); + const TeamCard({super.key, required this.team, required this.controller, required this.onFavoriteTap}); @override Widget build(BuildContext context) { return Card( - color: Colors.white, - elevation: 3, - margin: EdgeInsets.only(bottom: 12 * sf), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)), + 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 * sf, vertical: 8 * sf), - - // --- 1. IMAGEM + FAVORITO --- + contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 8 * context.sf), 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, + radius: 28 * context.sf, backgroundColor: Colors.grey[200], + backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http')) ? NetworkImage(team.imageUrl) : null, + child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http')) ? Text(team.imageUrl.isEmpty ? "🏀" : team.imageUrl, style: TextStyle(fontSize: 24 * context.sf)) : null, ), Positioned( - left: -15 * sf, - top: -10 * sf, + 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 * sf, - shadows: [ - Shadow( - color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), - blurRadius: 4 * sf, - ), - ], - ), + icon: Icon(team.isFavorite ? Icons.star : Icons.star_border, color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1), size: 28 * context.sf, shadows: [Shadow(color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), blurRadius: 4 * context.sf)]), onPressed: onFavoriteTap, ), ), ], ), - - // --- 2. TÍTULO --- - title: Text( - team.name, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf), - overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos - ), - - // --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) --- + title: Text(team.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * context.sf), overflow: TextOverflow.ellipsis), subtitle: Padding( - padding: EdgeInsets.only(top: 6.0 * sf), + padding: EdgeInsets.only(top: 6.0 * context.sf), child: Row( children: [ - Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey), - SizedBox(width: 4 * sf), - - // 👇 STREAMBUILDER EM VEZ DE FUTUREBUILDER 👇 + 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 * sf, - ), - ); + return Text("$count Jogs.", style: TextStyle(color: count > 0 ? Colors.green[700] : Colors.orange, fontWeight: FontWeight.bold, fontSize: 13 * context.sf)); }, ), - - SizedBox(width: 8 * sf), - Expanded( - child: Text( - "| ${team.season}", - style: TextStyle(color: Colors.grey, fontSize: 13 * sf), - overflow: TextOverflow.ellipsis, - ), - ), + SizedBox(width: 8 * context.sf), + Expanded(child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * context.sf), overflow: TextOverflow.ellipsis)), ], ), ), - - // --- 4. BOTÕES (Estatísticas e Apagar) --- trailing: Row( - mainAxisSize: MainAxisSize.min, // <-- ISTO RESOLVE O OVERFLOW DAS RISCAS AMARELAS + mainAxisSize: MainAxisSize.min, children: [ - IconButton( - tooltip: 'Ver Estatísticas', - icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * sf), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TeamStatsPage(team: team), - ), - ); - }, - ), - IconButton( - tooltip: 'Eliminar Equipa', - icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * sf), - onPressed: () => _confirmDelete(context), - ), + 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)), ], ), ), @@ -396,20 +280,11 @@ class TeamCard extends StatelessWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold)), - content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * sf)), + title: 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)), - ), - TextButton( - onPressed: () { - controller.deleteTeam(team.id); - Navigator.pop(context); - }, - child: Text('Eliminar', style: TextStyle(color: Colors.red, 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))), ], ), ); @@ -419,9 +294,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; // Recebe a escala - - const CreateTeamDialog({super.key, required this.onConfirm, required this.sf}); + const CreateTeamDialog({super.key, required this.onConfirm}); @override State createState() => _CreateTeamDialogState(); @@ -435,69 +308,31 @@ class _CreateTeamDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)), - title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold)), + 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), - 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( - 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(), + 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), - decoration: InputDecoration( - labelText: 'URL Imagem ou Emoji', - labelStyle: TextStyle(fontSize: 14 * widget.sf), - hintText: 'Ex: 🏀 ou https://...', - hintStyle: TextStyle(fontSize: 14 * widget.sf) - ), - ), + 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)) - ), + TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))), ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFE74C3C), - padding: EdgeInsets.symmetric(horizontal: 16 * 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)), + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)), + onPressed: () { if (_nameController.text.trim().isNotEmpty) { widget.onConfirm(_nameController.text.trim(), _selectedSeason, _imageController.text.trim()); Navigator.pop(context); } }, + child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * context.sf)), ), ], ); diff --git a/lib/utils/size_extension.dart b/lib/utils/size_extension.dart new file mode 100644 index 0000000..a0ff6f1 --- /dev/null +++ b/lib/utils/size_extension.dart @@ -0,0 +1,15 @@ +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 diff --git a/lib/widgets/login_widgets.dart b/lib/widgets/login_widgets.dart index 7565183..caafa5c 100644 --- a/lib/widgets/login_widgets.dart +++ b/lib/widgets/login_widgets.dart @@ -1,194 +1,146 @@ - import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:playmaker/controllers/login_controller.dart'; import 'package:playmaker/pages/RegisterPage.dart'; +import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER! - class BasketTrackHeader extends StatelessWidget { - const BasketTrackHeader({super.key}); - - @override - Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - - // TAMANHOS AUMENTADOS para tablets - final logoSize = screenWidth > 600 ? 400.0 : 300.0; // ↑ Aumentado - final titleFontSize = screenWidth > 600 ? 48.0 : 36.0; // ↑ Aumentado - final subtitleFontSize = screenWidth > 600 ? 22.0 : 18.0; // ↑ Aumentado - - return Column( - children: [ - Container( - width: logoSize, - height: logoSize, - child: Image.asset( - 'assets/playmaker-logo.png', - fit: BoxFit.contain, - ), - ), - SizedBox(height: screenWidth > 600 ? 1.0 : 1.0), - - Text( - 'BasketTrack', - style: TextStyle( - fontSize: titleFontSize, - fontWeight: FontWeight.bold, - color: Colors.grey[900], - ), - ), - SizedBox(height: screenWidth > 600 ? 1.0 : 1.0), - - Text( - 'Gere as tuas equipas e estatísticas', - style: TextStyle( - fontSize: subtitleFontSize, - color: Colors.grey[600], - fontWeight: FontWeight.w500, // ↑ Adicionado peso da fonte - ), - textAlign: TextAlign.center, - ), - ], - ); - } - } - - class LoginFormFields extends StatelessWidget { - final LoginController controller; - - const LoginFormFields({super.key, required this.controller}); +class BasketTrackHeader extends StatelessWidget { + const BasketTrackHeader({super.key}); @override Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - final verticalPadding = screenWidth > 600 ? 22.0 : 16.0; + return Column( + children: [ + SizedBox( + width: 300 * context.sf, // Ajusta o tamanho da imagem suavemente + height: 300 * context.sf, + child: Image.asset( + 'assets/playmaker-logo.png', + fit: BoxFit.contain, + ), + ), + Text( + 'BasketTrack', + style: TextStyle( + fontSize: 36 * context.sf, + fontWeight: FontWeight.bold, + color: Colors.grey[900], + ), + ), + SizedBox(height: 6 * context.sf), + Text( + 'Gere as tuas equipas e estatísticas', + style: TextStyle( + fontSize: 16 * context.sf, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ], + ); + } +} +class LoginFormFields extends StatelessWidget { + final LoginController controller; + + const LoginFormFields({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { return Column( children: [ TextField( controller: controller.emailController, + style: TextStyle(fontSize: 15 * context.sf), decoration: InputDecoration( labelText: 'E-mail', - prefixIcon: const Icon(Icons.email_outlined), - // O erro agora vem diretamente do controller + labelStyle: TextStyle(fontSize: 15 * context.sf), + prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf), errorText: controller.emailError, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), - contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), + contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), ), keyboardType: TextInputType.emailAddress, ), - const SizedBox(height: 20), + SizedBox(height: 20 * context.sf), TextField( controller: controller.passwordController, obscureText: controller.obscurePassword, + style: TextStyle(fontSize: 15 * context.sf), decoration: InputDecoration( labelText: 'Palavra-passe', - prefixIcon: const Icon(Icons.lock_outlined), + labelStyle: TextStyle(fontSize: 15 * context.sf), + prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf), errorText: controller.passwordError, suffixIcon: IconButton( - icon: Icon(controller.obscurePassword - ? Icons.visibility_outlined - : Icons.visibility_off_outlined), + icon: Icon( + controller.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, + size: 22 * context.sf + ), onPressed: controller.togglePasswordVisibility, ), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), - contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), + contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), ), ), ], ); } } - class LoginButton extends StatelessWidget { - final LoginController controller; - final VoidCallback onLoginSuccess; - const LoginButton({ - super.key, - required this.controller, - required this.onLoginSuccess, - }); +class LoginButton extends StatelessWidget { + final LoginController controller; + final VoidCallback onLoginSuccess; - @override - Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - - // BOTÕES MAIORES - final buttonHeight = screenWidth > 600 ? 70.0 : 58.0; // ↑ Aumentado - final fontSize = screenWidth > 600 ? 22.0 : 18.0; // ↑ Aumentado + const LoginButton({super.key, required this.controller, required this.onLoginSuccess}); - return SizedBox( - width: double.infinity, - height: buttonHeight, - child: ElevatedButton( - onPressed: controller.isLoading ? null : () async { - final success = await controller.login(); - if (success) { - onLoginSuccess(); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFE74C3C), - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), // ↑ Bordas mais arredondadas - ), - elevation: 3, // ↑ Sombra mais pronunciada - ), - child: controller.isLoading - ? SizedBox( - width: 28, // ↑ Aumentado - height: 28, // ↑ Aumentado - child: CircularProgressIndicator( - strokeWidth: 3, // ↑ Aumentado - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text( - 'Entrar', - style: TextStyle( - fontSize: fontSize, - fontWeight: FontWeight.w700, // - ), - ), + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: 58 * context.sf, + child: ElevatedButton( + onPressed: controller.isLoading ? null : () async { + final success = await controller.login(); + if (success) onLoginSuccess(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE74C3C), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)), + elevation: 3, ), - ); - } + child: controller.isLoading + ? SizedBox( + width: 28 * context.sf, height: 28 * context.sf, + child: const CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation(Colors.white)), + ) + : Text('Entrar', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), + ), + ); } +} class CreateAccountButton extends StatelessWidget { const CreateAccountButton({super.key}); @override Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - final buttonHeight = screenWidth > 600 ? 70.0 : 58.0; - final fontSize = screenWidth > 600 ? 22.0 : 18.0; - return SizedBox( width: double.infinity, - height: buttonHeight, + height: 58 * context.sf, child: OutlinedButton( onPressed: () { - // Navega para a página de registo que criaste - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const RegisterPage()), - ); + Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterPage())); }, style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFFE74C3C), - side: const BorderSide(color: Color(0xFFE74C3C), width: 2), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - ), - child: Text( - 'Criar Conta', - style: TextStyle( - fontSize: fontSize, - fontWeight: FontWeight.w700, - ), + side: BorderSide(color: const Color(0xFFE74C3C), width: 2 * context.sf), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)), ), + child: Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), ), ); } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/lib/widgets/register_widgets.dart b/lib/widgets/register_widgets.dart index 3982b01..da3528d 100644 --- a/lib/widgets/register_widgets.dart +++ b/lib/widgets/register_widgets.dart @@ -1,41 +1,24 @@ import 'package:flutter/material.dart'; -import '../controllers/register_controller.dart'; // Garante que o caminho está certo +import '../controllers/register_controller.dart'; +import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER! class RegisterHeader extends StatelessWidget { const RegisterHeader({super.key}); @override Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - - final logoSize = screenWidth > 600 ? 150.0 : 100.0; - final titleFontSize = screenWidth > 600 ? 48.0 : 36.0; - final subtitleFontSize = screenWidth > 600 ? 22.0 : 18.0; - return Column( children: [ - Icon( - Icons.person_add_outlined, - size: logoSize, - color: const Color(0xFFE74C3C) - ), - const SizedBox(height: 10), + Icon(Icons.person_add_outlined, size: 100 * context.sf, color: const Color(0xFFE74C3C)), + SizedBox(height: 10 * context.sf), Text( 'Nova Conta', - style: TextStyle( - fontSize: titleFontSize, - fontWeight: FontWeight.bold, - color: Colors.grey[900], - ), + style: TextStyle(fontSize: 36 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[900]), ), - const SizedBox(height: 5), + SizedBox(height: 5 * context.sf), Text( 'Cria o teu perfil no BasketTrack', - style: TextStyle( - fontSize: subtitleFontSize, - color: Colors.grey[600], - fontWeight: FontWeight.w500, - ), + style: TextStyle(fontSize: 16 * context.sf, color: Colors.grey[600], fontWeight: FontWeight.w500), textAlign: TextAlign.center, ), ], @@ -45,7 +28,6 @@ class RegisterHeader extends StatelessWidget { class RegisterFormFields extends StatefulWidget { final RegisterController controller; - const RegisterFormFields({super.key, required this.controller}); @override @@ -57,69 +39,68 @@ class _RegisterFormFieldsState extends State { @override Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - final verticalPadding = screenWidth > 600 ? 22.0 : 16.0; - - // IMPORTANTE: Envolvemos tudo num Form usando a chave do controller return Form( key: widget.controller.formKey, child: Column( children: [ - // Campo Nome (Opcional, mas útil) TextFormField( controller: widget.controller.nameController, + style: TextStyle(fontSize: 15 * context.sf), decoration: InputDecoration( labelText: 'Nome Completo', - prefixIcon: const Icon(Icons.person_outline), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), - contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16), + labelStyle: TextStyle(fontSize: 15 * context.sf), + prefixIcon: Icon(Icons.person_outline, size: 22 * context.sf), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), + contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), ), ), - const SizedBox(height: 20), + SizedBox(height: 20 * context.sf), - // Campo Email TextFormField( controller: widget.controller.emailController, - // Validação automática ligada ao controller validator: widget.controller.validateEmail, + style: TextStyle(fontSize: 15 * context.sf), decoration: InputDecoration( labelText: 'E-mail', - prefixIcon: const Icon(Icons.email_outlined), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), - contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16), + labelStyle: TextStyle(fontSize: 15 * context.sf), + prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), + contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), ), keyboardType: TextInputType.emailAddress, ), - const SizedBox(height: 20), + SizedBox(height: 20 * context.sf), - // Campo Password TextFormField( controller: widget.controller.passwordController, obscureText: _obscurePassword, validator: widget.controller.validatePassword, + style: TextStyle(fontSize: 15 * context.sf), decoration: InputDecoration( labelText: 'Palavra-passe', - prefixIcon: const Icon(Icons.lock_outlined), + labelStyle: TextStyle(fontSize: 15 * context.sf), + prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf), suffixIcon: IconButton( - icon: Icon(_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined), + icon: Icon(_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 22 * context.sf), onPressed: () => setState(() => _obscurePassword = !_obscurePassword), ), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), - contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), + contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), ), ), - const SizedBox(height: 20), + SizedBox(height: 20 * context.sf), - // Campo Confirmar Password TextFormField( controller: widget.controller.confirmPasswordController, obscureText: _obscurePassword, validator: widget.controller.validateConfirmPassword, + style: TextStyle(fontSize: 15 * context.sf), decoration: InputDecoration( labelText: 'Confirmar Palavra-passe', - prefixIcon: const Icon(Icons.lock_clock_outlined), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), - contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16), + labelStyle: TextStyle(fontSize: 15 * context.sf), + prefixIcon: Icon(Icons.lock_clock_outlined, size: 22 * context.sf), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)), + contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf), ), ), ], @@ -130,49 +111,27 @@ class _RegisterFormFieldsState extends State { class RegisterButton extends StatelessWidget { final RegisterController controller; - - const RegisterButton({ - super.key, - required this.controller, - }); + const RegisterButton({super.key, required this.controller}); @override Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - - final buttonHeight = screenWidth > 600 ? 70.0 : 58.0; - final fontSize = screenWidth > 600 ? 22.0 : 18.0; - return SizedBox( width: double.infinity, - height: buttonHeight, + height: 58 * context.sf, child: ElevatedButton( - // Passamos o context para o controller lidar com as SnackBars e Navegação onPressed: controller.isLoading ? null : () => controller.signUp(context), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFE74C3C), foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)), elevation: 3, ), child: controller.isLoading - ? const SizedBox( - width: 28, - height: 28, - child: CircularProgressIndicator( - strokeWidth: 3, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), + ? SizedBox( + width: 28 * context.sf, height: 28 * context.sf, + child: const CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation(Colors.white)), ) - : Text( - 'Criar Conta', - style: TextStyle( - fontSize: fontSize, - fontWeight: FontWeight.w700, - ), - ), + : Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), ), ); } diff --git a/pubspec.lock b/pubspec.lock index bf5456b..720ba58 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" clock: dependency: transitive description: @@ -268,18 +268,18 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -553,10 +553,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 036a8bf..590c58f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,7 @@ 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