login e register

This commit is contained in:
2026-03-12 10:42:21 +00:00
parent f5d7e88149
commit b95d6dc8d4
13 changed files with 926 additions and 1122 deletions

BIN
assets/playmaker-logos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

View File

@@ -1,8 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:playmaker/controllers/placar_controller.dart'; import 'package:playmaker/controllers/placar_controller.dart';
import 'package:playmaker/widgets/placar_widgets.dart'; import '../utils/size_extension.dart'; // 👇 EXTENSÃO IMPORTADA
import 'dart:math' as math;
class PlacarPage extends StatefulWidget { class PlacarPage extends StatefulWidget {
final String gameId, myTeam, opponentTeam; final String gameId, myTeam, opponentTeam;
@@ -42,30 +41,20 @@ class _PlacarPageState extends State<PlacarPage> {
} }
// --- BOTÕES FLUTUANTES DE FALTA --- // --- 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( return Positioned(
top: top, top: top, left: left > 0 ? left : null, right: right > 0 ? right : null,
left: left > 0 ? left : null,
right: right > 0 ? right : null,
child: Draggable<String>( child: Draggable<String>(
data: action, data: action,
feedback: Material( feedback: Material(
color: Colors.transparent, color: Colors.transparent,
child: CircleAvatar( child: CircleAvatar(radius: 30 * context.sf, backgroundColor: color.withOpacity(0.8), child: Icon(icon, color: Colors.white, size: 30 * context.sf)),
radius: 30 * sf,
backgroundColor: color.withOpacity(0.8),
child: Icon(icon, color: Colors.white, size: 30 * sf)
),
), ),
child: Column( child: Column(
children: [ children: [
CircleAvatar( CircleAvatar(radius: 27 * context.sf, backgroundColor: color, child: Icon(icon, color: Colors.white, size: 28 * context.sf)),
radius: 27 * sf, SizedBox(height: 5 * context.sf),
backgroundColor: color, Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 * context.sf)),
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)),
], ],
), ),
), ),
@@ -75,16 +64,13 @@ class _PlacarPageState extends State<PlacarPage> {
// --- BOTÕES LATERAIS QUADRADOS --- // --- BOTÕES LATERAIS QUADRADOS ---
Widget _buildCornerBtn({required String heroTag, required IconData icon, required Color color, required VoidCallback onTap, required double size, bool isLoading = false}) { Widget _buildCornerBtn({required String heroTag, required IconData icon, required Color color, required VoidCallback onTap, required double size, bool isLoading = false}) {
return SizedBox( return SizedBox(
width: size, width: size, height: size,
height: size,
child: FloatingActionButton( child: FloatingActionButton(
heroTag: heroTag, heroTag: heroTag, backgroundColor: color,
backgroundColor: color,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * (size / 50))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * (size / 50))),
elevation: 5, elevation: 5, onPressed: isLoading ? null : onTap,
onPressed: isLoading ? null : onTap,
child: isLoading 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), : Icon(icon, color: Colors.white, size: size * 0.55),
), ),
); );
@@ -95,10 +81,7 @@ class _PlacarPageState extends State<PlacarPage> {
final double wScreen = MediaQuery.of(context).size.width; final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height; final double hScreen = MediaQuery.of(context).size.height;
// 👇 DIVISOR AUMENTADO PARA O 'sf' FICAR MAIS PEQUENO 👇 final double cornerBtnSize = 48 * context.sf;
final double sf = math.min(wScreen / 1150, hScreen / 720);
final double cornerBtnSize = 48 * sf; // Tamanho ideal (Nem 38 nem 55)
if (_controller.isLoading) { if (_controller.isLoading) {
return Scaffold( return Scaffold(
@@ -107,20 +90,18 @@ class _PlacarPageState extends State<PlacarPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text("PREPARANDO O PAVILHÃO", style: TextStyle(color: Colors.white24, fontSize: 45 * sf, fontWeight: FontWeight.bold, letterSpacing: 2)), Text("PREPARANDO O PAVILHÃO", style: TextStyle(color: Colors.white24, fontSize: 45 * context.sf, fontWeight: FontWeight.bold, letterSpacing: 2)),
SizedBox(height: 35 * sf), SizedBox(height: 35 * context.sf),
StreamBuilder( StreamBuilder(
stream: Stream.periodic(const Duration(seconds: 3)), stream: Stream.periodic(const Duration(seconds: 3)),
builder: (context, snapshot) { builder: (context, snapshot) {
List<String> frases = [ List<String> frases = [
"O Treinador está a desenhar a tática...", "O Treinador está a desenhar a tática...", "A encher as bolas com ar de campeão...",
"A encher as bolas com ar de campeão...", "O árbitro está a testar o apito...", "A verificar se o cesto está nivelado...",
"O árbitro está a testar o apito...",
"A verificar se o cesto está nivelado...",
"Os jogadores estão a terminar o aquecimento..." "Os jogadores estão a terminar o aquecimento..."
]; ];
String frase = frases[DateTime.now().second % frases.length]; 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<PlacarPage> {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFF266174), backgroundColor: const Color(0xFF266174),
body: SafeArea( body: SafeArea(
top: false, top: false, bottom: false,
bottom: false,
child: Stack( child: Stack(
children: [ children: [
// --- O CAMPO --- // --- O CAMPO ---
Container( 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)), decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.5)),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
@@ -155,14 +135,12 @@ class _PlacarPageState extends State<PlacarPage> {
}, },
child: Container( child: Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
image: DecorationImage( image: DecorationImage(image: AssetImage('assets/campo.png'), fit: BoxFit.fill),
image: AssetImage('assets/campo.png'), ),
fit: BoxFit.fill, // <-- A MÁGICA ESTÁ AQUI
), ),
child: Stack( child: Stack(
children: _controller.matchShots.map((shot) => Positioned( children: _controller.matchShots.map((shot) => Positioned(
left: shot.position.dx - (9 * sf), top: shot.position.dy - (9 * sf), left: shot.position.dx - (9 * context.sf), top: shot.position.dy - (9 * context.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)), 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(), )).toList(),
), ),
), ),
@@ -170,47 +148,47 @@ image: DecorationImage(
// --- JOGADORES --- // --- JOGADORES ---
if (!_controller.isSelectingShotLocation) ...[ 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.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, sf: sf)), 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, sf: sf)), 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, sf: sf)), 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, sf: sf)), 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.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, sf: sf)), 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, sf: sf)), 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, sf: sf)), 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, sf: sf)), Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[4], isOpponent: true)),
], ],
// --- BOTÕES DE FALTAS --- // --- BOTÕES DE FALTAS ---
if (!_controller.isSelectingShotLocation) ...[ if (!_controller.isSelectingShotLocation) ...[
_buildFloatingFoulBtn("FALTA +", Colors.orange, "add_foul", Icons.sports, w * 0.39, 0.0, 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, sf), _buildFloatingFoulBtn("FALTA -", Colors.redAccent, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31),
], ],
// --- BOTÃO PLAY/PAUSE --- // --- BOTÃO PLAY/PAUSE ---
if (!_controller.isSelectingShotLocation) if (!_controller.isSelectingShotLocation)
Positioned( 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 --- // --- 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 --- // --- 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 --- // --- OVERLAY LANÇAMENTO ---
if (_controller.isSelectingShotLocation) if (_controller.isSelectingShotLocation)
@@ -218,9 +196,9 @@ image: DecorationImage(
top: h * 0.4, left: 0, right: 0, top: h * 0.4, left: 0, right: 0,
child: Center( child: Center(
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 35 * sf, vertical: 18 * sf), padding: EdgeInsets.symmetric(horizontal: 35 * context.sf, vertical: 18 * context.sf),
decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(11 * sf), border: Border.all(color: Colors.white, width: 1.5 * 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 * sf, fontWeight: FontWeight.bold)), 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 --- // --- BOTÕES LATERAIS ---
// Topo Esquerdo: Guardar e Sair (Botão Único) Positioned(
Positioned( top: 50 * context.sf, left: 12 * context.sf,
top: 50 * sf, left: 12 * sf, child: _buildCornerBtn(
child: _buildCornerBtn( heroTag: 'btn_save_exit', icon: Icons.save_alt, color: const Color(0xFFD92C2C), size: cornerBtnSize, isLoading: _controller.isSaving,
heroTag: 'btn_save_exit', onTap: () async {
icon: Icons.save_alt, // Mudei o ícone para dar a ideia de "Guardar e Sair" await _controller.saveGameStats(context);
color: const Color(0xFFD92C2C), // Mantive vermelho para saberes que é para fechar if (context.mounted) Navigator.pop(context);
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)
),
],
),
), ),
),
// Base Direita: Banco Visitante + TIMEOUT DO VISITANTE // Base Esquerda: Banco Casa + TIMEOUT DA CASA
Positioned( Positioned(
bottom: 55 * sf, right: 12 * sf, bottom: 55 * context.sf, left: 12 * context.sf,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (_controller.showOppBench) BenchPlayersList(controller: _controller, isOpponent: true, sf: sf), if (_controller.showMyBench) BenchPlayersList(controller: _controller, isOpponent: false),
SizedBox(height: 12 * sf), 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(); }), _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), SizedBox(height: 12 * context.sf),
_buildCornerBtn( _buildCornerBtn(
heroTag: 'btn_to_away', heroTag: 'btn_to_home', icon: Icons.timer, color: _controller.myTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFF1E5BB2), size: cornerBtnSize,
icon: Icons.timer, 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)
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 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<Widget> 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<String>(
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<String>(
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<String>(
onAcceptWithDetails: (details) {
final action = details.data;
if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) controller.handleActionDrag(context, action, "$prefix$name");
else if (action.startsWith("bench_")) 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<String, int> 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<Widget> 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<String>(
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<String>(
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))),
],
);
}
} }

View File

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

View File

@@ -3,114 +3,83 @@ import 'package:playmaker/pages/PlacarPage.dart';
import '../controllers/game_controller.dart'; import '../controllers/game_controller.dart';
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../models/game_model.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 --- // --- CARD DE EXIBIÇÃO DO JOGO ---
class GameResultCard extends StatelessWidget { class GameResultCard extends StatelessWidget {
final String gameId; final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
final String myTeam, opponentTeam, myScore, opponentScore, status, season; final String? myTeamLogo, opponentTeamLogo;
final String? myTeamLogo;
final String? opponentTeamLogo;
final double sf;
const GameResultCard({ const GameResultCard({
super.key, super.key, required this.gameId, required this.myTeam, required this.opponentTeam,
required this.gameId, required this.myScore, required this.opponentScore, required this.status, required this.season,
required this.myTeam, this.myTeamLogo, this.opponentTeamLogo,
required this.opponentTeam,
required this.myScore,
required this.opponentScore,
required this.status,
required this.season,
this.myTeamLogo,
this.opponentTeamLogo,
required this.sf,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
margin: EdgeInsets.only(bottom: 16 * sf), margin: EdgeInsets.only(bottom: 16 * context.sf),
padding: EdgeInsets.all(16 * sf), padding: EdgeInsets.all(16 * context.sf),
decoration: BoxDecoration( decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * context.sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * context.sf)]),
color: Colors.white,
borderRadius: BorderRadius.circular(20 * sf),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)],
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, sf)), Expanded(child: _buildTeamInfo(context, myTeam, const Color(0xFFE74C3C), myTeamLogo)),
_buildScoreCenter(context, gameId, sf), _buildScoreCenter(context, gameId),
Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, sf)), 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( return Column(
children: [ children: [
CircleAvatar( 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),
radius: 24 * sf, SizedBox(height: 6 * context.sf),
backgroundColor: color, Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2),
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,
),
], ],
); );
} }
Widget _buildScoreCenter(BuildContext context, String id, double sf) { Widget _buildScoreCenter(BuildContext context, String id) {
return Column( return Column(
children: [ children: [
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_scoreBox(myScore, Colors.green, sf), _scoreBox(context, myScore, Colors.green),
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf)), Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * context.sf)),
_scoreBox(opponentScore, Colors.grey, sf), _scoreBox(context, opponentScore, Colors.grey),
], ],
), ),
SizedBox(height: 10 * sf), SizedBox(height: 10 * context.sf),
TextButton.icon( TextButton.icon(
onPressed: () { onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))),
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)),
icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: const Color(0xFFE74C3C)), 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),
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,
),
), ),
SizedBox(height: 6 * sf), SizedBox(height: 6 * context.sf),
Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)), Text(status, style: TextStyle(fontSize: 12 * context.sf, color: Colors.blue, fontWeight: FontWeight.bold)),
], ],
); );
} }
Widget _scoreBox(String pts, Color c, double sf) => Container( Widget _scoreBox(BuildContext context, String pts, Color c) => Container(
padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf), padding: EdgeInsets.symmetric(horizontal: 12 * context.sf, vertical: 6 * context.sf),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)), decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * context.sf)),
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)), child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)),
); );
} }
// --- POPUP DE CRIAÇÃO --- // --- POPUP DE CRIAÇÃO ---
class CreateGameDialogManual extends StatefulWidget { class CreateGameDialogManual extends StatefulWidget {
final TeamController teamController; final TeamController teamController;
final GameController gameController; final GameController gameController;
final double sf;
const CreateGameDialogManual({super.key, required this.teamController, required this.gameController, required this.sf}); const CreateGameDialogManual({super.key, required this.teamController, required this.gameController});
@override @override
State<CreateGameDialogManual> createState() => _CreateGameDialogManualState(); State<CreateGameDialogManual> createState() => _CreateGameDialogManualState();
@@ -136,27 +105,24 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * widget.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * widget.sf)), title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField( 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))),
controller: _seasonController, style: TextStyle(fontSize: 14 * widget.sf), SizedBox(height: 15 * context.sf),
decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * widget.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * widget.sf)), _buildSearch(context, "Minha Equipa", _myTeamController),
), Padding(padding: EdgeInsets.symmetric(vertical: 10 * context.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * context.sf))),
SizedBox(height: 15 * widget.sf), _buildSearch(context, "Adversário", _opponentController),
_buildSearch(label: "Minha Equipa", controller: _myTeamController, sf: widget.sf),
Padding(padding: EdgeInsets.symmetric(vertical: 10 * widget.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * widget.sf))),
_buildSearch(label: "Adversário", controller: _opponentController, sf: widget.sf),
], ],
), ),
), ),
actions: [ 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( 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 { onPressed: _isLoading ? null : () async {
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) { if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
setState(() => _isLoading = true); setState(() => _isLoading = true);
@@ -168,13 +134,13 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
} }
} }
}, },
child: _isLoading ? SizedBox(width: 20 * widget.sf, height: 20 * widget.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * widget.sf)), 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<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: widget.teamController.teamsStream, stream: widget.teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -190,9 +156,9 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
return Align( return Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Material( child: Material(
elevation: 4.0, borderRadius: BorderRadius.circular(8 * sf), elevation: 4.0, borderRadius: BorderRadius.circular(8 * context.sf),
child: ConstrainedBox( 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( child: ListView.builder(
padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
@@ -200,8 +166,8 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
final String name = option['name'].toString(); final String name = option['name'].toString();
final String? imageUrl = option['image_url']; final String? imageUrl = option['image_url'];
return ListTile( return ListTile(
leading: CircleAvatar(radius: 20 * 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), 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 * sf)), title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * context.sf)),
onTap: () { onSelected(option); }, onTap: () { onSelected(option); },
); );
}, },
@@ -211,11 +177,11 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
); );
}, },
fieldViewBuilder: (ctx, txtCtrl, node, submit) { 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; }); txtCtrl.addListener(() { controller.text = txtCtrl.text; });
return TextField( return TextField(
controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * sf), controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * context.sf),
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * sf), prefixIcon: Icon(Icons.search, size: 20 * sf), border: const OutlineInputBorder()), 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<CreateGameDialogManual> {
} }
} }
// --- PÁGINA PRINCIPAL DOS JOGOS COM FILTROS --- // --- PÁGINA PRINCIPAL DOS JOGOS ---
class GamePage extends StatefulWidget { class GamePage extends StatefulWidget {
const GamePage({super.key}); const GamePage({super.key});
@@ -235,96 +201,54 @@ class GamePage extends StatefulWidget {
class _GamePageState extends State<GamePage> { class _GamePageState extends State<GamePage> {
final GameController gameController = GameController(); final GameController gameController = GameController();
final TeamController teamController = TeamController(); final TeamController teamController = TeamController();
// Variáveis para os filtros
String selectedSeason = 'Todas'; String selectedSeason = 'Todas';
String selectedTeam = 'Todas'; String selectedTeam = 'Todas';
@override @override
Widget build(BuildContext context) { 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'; bool isFilterActive = selectedSeason != 'Todas' || selectedTeam != 'Todas';
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F7FA), backgroundColor: const Color(0xFFF5F7FA),
appBar: AppBar( 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, backgroundColor: Colors.white,
elevation: 0, elevation: 0,
actions: [ actions: [
// 👇 BOTÃO DE FILTRO NA APP BAR 👇
Padding( Padding(
padding: EdgeInsets.only(right: 8.0 * sf), padding: EdgeInsets.only(right: 8.0 * context.sf),
child: IconButton( child: IconButton(
icon: Icon( icon: Icon(isFilterActive ? Icons.filter_list_alt : Icons.filter_list, color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87, size: 26 * context.sf),
isFilterActive ? Icons.filter_list_alt : Icons.filter_list, onPressed: () => _showFilterPopup(context),
color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87,
size: 26 * sf,
),
onPressed: () => _showFilterPopup(context, sf),
), ),
) )
], ],
), ),
body: StreamBuilder<List<Map<String, dynamic>>>( body: StreamBuilder<List<Map<String, dynamic>>>(
stream: teamController.teamsStream, stream: teamController.teamsStream,
builder: (context, teamSnapshot) { builder: (context, teamSnapshot) {
final List<Map<String, dynamic>> teamsList = teamSnapshot.data ?? []; final List<Map<String, dynamic>> teamsList = teamSnapshot.data ?? [];
return StreamBuilder<List<Game>>( return StreamBuilder<List<Game>>(
stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason), stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason),
builder: (context, gameSnapshot) { builder: (context, gameSnapshot) {
if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) { if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) return const Center(child: CircularProgressIndicator());
return const Center(child: CircularProgressIndicator()); if (gameSnapshot.hasError) return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * context.sf)));
}
if (gameSnapshot.hasError) {
return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * sf)));
}
if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) { if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) {
return Center( 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))]));
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 ListView.builder( return ListView.builder(
padding: EdgeInsets.all(16 * sf), padding: EdgeInsets.all(16 * context.sf),
itemCount: gameSnapshot.data!.length, itemCount: gameSnapshot.data!.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final game = gameSnapshot.data![index]; final game = gameSnapshot.data![index];
String? myLogo, oppLogo;
String? myLogo;
String? oppLogo;
for (var team in teamsList) { for (var team in teamsList) {
if (team['name'] == game.myTeam) { myLogo = team['image_url']; } if (team['name'] == game.myTeam) myLogo = team['image_url'];
if (team['name'] == game.opponentTeam) { oppLogo = team['image_url']; } if (team['name'] == game.opponentTeam) oppLogo = team['image_url'];
} }
return GameResultCard( return GameResultCard(
gameId: game.id, gameId: game.id, myTeam: game.myTeam, opponentTeam: game.opponentTeam, myScore: game.myScore,
myTeam: game.myTeam, opponentScore: game.opponentScore, status: game.status, season: game.season, myTeamLogo: myLogo, opponentTeamLogo: oppLogo,
opponentTeam: game.opponentTeam,
myScore: game.myScore,
opponentScore: game.opponentScore,
status: game.status,
season: game.season,
myTeamLogo: myLogo,
opponentTeamLogo: oppLogo,
sf: sf,
); );
}, },
); );
@@ -334,92 +258,60 @@ class _GamePageState extends State<GamePage> {
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
backgroundColor: const Color(0xFFE74C3C), backgroundColor: const Color(0xFFE74C3C),
child: Icon(Icons.add, color: Colors.white, size: 24 * sf), child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
onPressed: () => _showCreateDialog(context, sf), onPressed: () => showDialog(context: context, builder: (context) => CreateGameDialogManual(teamController: teamController, gameController: gameController)),
), ),
); );
} }
// 👇 O POPUP DE FILTROS 👇 void _showFilterPopup(BuildContext context) {
void _showFilterPopup(BuildContext context, double sf) {
// Variáveis temporárias para o Popup (para não atualizar a lista antes de clicar em "Aplicar")
String tempSeason = selectedSeason; String tempSeason = selectedSeason;
String tempTeam = selectedTeam; String tempTeam = selectedTeam;
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
// StatefulBuilder permite atualizar a interface APENAS dentro do Popup
return StatefulBuilder( return StatefulBuilder(
builder: (context, setPopupState) { builder: (context, setPopupState) {
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * sf)), Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)),
IconButton( IconButton(icon: const Icon(Icons.close, color: Colors.grey), onPressed: () => Navigator.pop(context), padding: EdgeInsets.zero, constraints: const BoxConstraints())
icon: const Icon(Icons.close, color: Colors.grey),
onPressed: () => Navigator.pop(context),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
)
], ],
), ),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 1. Filtro de Temporada Text("Temporada", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
Text("Temporada", style: TextStyle(fontSize: 12 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), SizedBox(height: 6 * context.sf),
SizedBox(height: 6 * sf),
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 12 * sf), padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * sf)),
child: DropdownButtonHideUnderline( child: DropdownButtonHideUnderline(
child: DropdownButton<String>( child: DropdownButton<String>(
isExpanded: true, isExpanded: true, value: tempSeason, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold),
value: tempSeason, items: ['Todas', '2024/25', '2025/26'].map((String value) => DropdownMenuItem<String>(value: value, child: Text(value))).toList(),
style: TextStyle(fontSize: 14 * sf, color: Colors.black87, fontWeight: FontWeight.bold), onChanged: (newValue) => setPopupState(() => tempSeason = newValue!),
items: ['Todas', '2024/25', '2025/26'].map((String value) {
return DropdownMenuItem<String>(value: value, child: Text(value));
}).toList(),
onChanged: (newValue) {
setPopupState(() => tempSeason = newValue!);
},
), ),
), ),
), ),
SizedBox(height: 20 * context.sf),
SizedBox(height: 20 * sf), Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * context.sf),
// 2. Filtro de Equipa
Text("Equipa", style: TextStyle(fontSize: 12 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * sf),
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 12 * sf), padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * sf)),
child: StreamBuilder<List<Map<String, dynamic>>>( child: StreamBuilder<List<Map<String, dynamic>>>(
stream: teamController.teamsStream, stream: teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
List<String> teamNames = ['Todas']; List<String> teamNames = ['Todas'];
if (snapshot.hasData) { if (snapshot.hasData) teamNames.addAll(snapshot.data!.map((t) => t['name'].toString()));
teamNames.addAll(snapshot.data!.map((t) => t['name'].toString()));
}
if (!teamNames.contains(tempTeam)) tempTeam = 'Todas'; if (!teamNames.contains(tempTeam)) tempTeam = 'Todas';
return DropdownButtonHideUnderline( return DropdownButtonHideUnderline(
child: DropdownButton<String>( child: DropdownButton<String>(
isExpanded: true, isExpanded: true, value: tempTeam, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold),
value: tempTeam, items: teamNames.map((String value) => DropdownMenuItem<String>(value: value, child: Text(value, overflow: TextOverflow.ellipsis))).toList(),
style: TextStyle(fontSize: 14 * sf, color: Colors.black87, fontWeight: FontWeight.bold), onChanged: (newValue) => setPopupState(() => tempTeam = newValue!),
items: teamNames.map((String value) {
return DropdownMenuItem<String>(value: value, child: Text(value, overflow: TextOverflow.ellipsis));
}).toList(),
onChanged: (newValue) {
setPopupState(() => tempTeam = newValue!);
},
), ),
); );
} }
@@ -428,32 +320,8 @@ class _GamePageState extends State<GamePage> {
], ],
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: () { setState(() { selectedSeason = 'Todas'; selectedTeam = 'Todas'; }); Navigator.pop(context); }, child: Text('LIMPAR', style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey))),
onPressed: () { 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))),
// 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)),
),
], ],
); );
} }
@@ -461,15 +329,4 @@ class _GamePageState extends State<GamePage> {
}, },
); );
} }
void _showCreateDialog(BuildContext context, double sf) {
showDialog(
context: context,
builder: (context) => CreateGameDialogManual(
teamController: teamController,
gameController: gameController,
sf: sf,
),
);
}
} }

View File

@@ -6,8 +6,7 @@ import 'package:playmaker/pages/teamPage.dart';
import 'package:playmaker/controllers/team_controller.dart'; import 'package:playmaker/controllers/team_controller.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:playmaker/pages/status_page.dart'; import 'package:playmaker/pages/status_page.dart';
import 'dart:math' as math; import '../utils/size_extension.dart';
import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart'; import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
@@ -31,12 +30,10 @@ class _HomeScreenState extends State<HomeScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width; // Já não precisamos calcular o sf aqui!
final double hScreen = MediaQuery.of(context).size.height;
final double sf = math.min(wScreen, hScreen) / 400;
final List<Widget> pages = [ final List<Widget> pages = [
_buildHomeContent(sf, wScreen), _buildHomeContent(context), // Passamos só o context
const GamePage(), const GamePage(),
const TeamsPage(), const TeamsPage(),
const StatusPage(), const StatusPage(),
@@ -45,11 +42,11 @@ class _HomeScreenState extends State<HomeScreen> {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: AppBar( appBar: AppBar(
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * sf)), title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)),
backgroundColor: HomeConfig.primaryColor, backgroundColor: HomeConfig.primaryColor,
foregroundColor: Colors.white, foregroundColor: Colors.white,
leading: IconButton( leading: IconButton(
icon: Icon(Icons.person, size: 24 * sf), icon: Icon(Icons.person, size: 24 * context.sf),
onPressed: () {}, onPressed: () {},
), ),
), ),
@@ -65,7 +62,8 @@ class _HomeScreenState extends State<HomeScreen> {
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
elevation: 1, elevation: 1,
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 [ destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'), NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'),
NavigationDestination(icon: Icon(Icons.sports_soccer_outlined), selectedIcon: Icon(Icons.sports_soccer), label: 'Jogo'), NavigationDestination(icon: Icon(Icons.sports_soccer_outlined), selectedIcon: Icon(Icons.sports_soccer), label: 'Jogo'),
@@ -76,16 +74,16 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
void _showTeamSelector(BuildContext context, double sf) { void _showTeamSelector(BuildContext context) {
showModalBottomSheet( showModalBottomSheet(
context: context, 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) { builder: (context) {
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: _teamController.teamsStream, stream: _teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * 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!; final teams = snapshot.data!;
return ListView.builder( return ListView.builder(
@@ -114,8 +112,9 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
Widget _buildHomeContent(double sf, double wScreen) { Widget _buildHomeContent(BuildContext context) {
final double cardHeight = (wScreen / 2) * 1.0; final double wScreen = MediaQuery.of(context).size.width;
final double cardHeight = wScreen * 0.5;
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: _selectedTeamId != null stream: _selectedTeamId != null
@@ -126,44 +125,44 @@ class _HomeScreenState extends State<HomeScreen> {
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
InkWell( InkWell(
onTap: () => _showTeamSelector(context, sf), onTap: () => _showTeamSelector(context),
child: Container( child: Container(
padding: EdgeInsets.all(12 * sf), padding: EdgeInsets.all(12 * context.sf),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * sf), border: Border.all(color: Colors.grey.shade300)), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.shade300)),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ 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), const Icon(Icons.arrow_drop_down),
], ],
), ),
), ),
), ),
SizedBox(height: 20 * sf), SizedBox(height: 20 * context.sf),
SizedBox( SizedBox(
height: cardHeight, height: cardHeight,
child: Row( child: Row(
children: [ children: [
Expanded(child: _buildStatCard(title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)), Expanded(child: _buildStatCard(context: context, title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)),
SizedBox(width: 12 * sf), SizedBox(width: 12 * context.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: '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( SizedBox(
height: cardHeight, height: cardHeight,
child: Row( child: Row(
children: [ children: [
Expanded(child: _buildStatCard(title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))), Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))),
SizedBox(width: 12 * sf), SizedBox(width: 12 * context.sf),
Expanded( Expanded(
child: PieChartCard( child: PieChartCard(
victories: _teamWins, victories: _teamWins,
@@ -172,58 +171,42 @@ class _HomeScreenState extends State<HomeScreen> {
title: 'DESEMPENHO', title: 'DESEMPENHO',
subtitle: 'Temporada', subtitle: 'Temporada',
backgroundColor: const Color(0xFFC62828), 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])), Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[800])),
SizedBox(height: 16 * sf), 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" _selectedTeamName == "Selecionar Equipa"
? Container( ? Container(
padding: EdgeInsets.all(20 * sf), padding: EdgeInsets.all(20 * context.sf),
alignment: Alignment.center, 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<List<Map<String, dynamic>>>( : StreamBuilder<List<Map<String, dynamic>>>(
// Pede os jogos ordenados pela data (sem filtros rígidos aqui)
stream: _supabase.from('games').stream(primaryKey: ['id']) stream: _supabase.from('games').stream(primaryKey: ['id'])
.order('game_date', ascending: false), .order('game_date', ascending: false),
builder: (context, gameSnapshot) { builder: (context, gameSnapshot) {
if (gameSnapshot.hasError) { if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
return Text("Erro ao carregar jogos: ${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 todosOsJogos = gameSnapshot.data ?? [];
final gamesList = todosOsJogos.where((game) { final gamesList = todosOsJogos.where((game) {
String myT = game['my_team']?.toString() ?? ''; String myT = game['my_team']?.toString() ?? '';
String oppT = game['opponent_team']?.toString() ?? ''; String oppT = game['opponent_team']?.toString() ?? '';
String status = game['status']?.toString() ?? ''; String status = game['status']?.toString() ?? '';
return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado';
// O jogo tem de envolver a equipa selecionada E estar Terminado }).take(3).toList();
bool isPlaying = (myT == _selectedTeamName || oppT == _selectedTeamName);
bool isFinished = status == 'Terminado';
return isPlaying && isFinished;
}).take(3).toList(); // Pega apenas nos 3 mais recentes
if (gamesList.isEmpty) { if (gamesList.isEmpty) {
return Container( return Container(
padding: EdgeInsets.all(20 * sf), padding: EdgeInsets.all(20 * context.sf),
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)), decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)),
alignment: Alignment.center, alignment: Alignment.center,
child: Text("Ainda não há jogos terminados para $_selectedTeamName.", style: TextStyle(color: Colors.grey)), child: Text("Ainda não há jogos terminados para $_selectedTeamName.", style: TextStyle(color: Colors.grey)),
@@ -232,47 +215,33 @@ class _HomeScreenState extends State<HomeScreen> {
return Column( return Column(
children: gamesList.map((game) { children: gamesList.map((game) {
// Lê os dados brutos da base de dados
String dbMyTeam = game['my_team']?.toString() ?? ''; String dbMyTeam = game['my_team']?.toString() ?? '';
String dbOppTeam = game['opponent_team']?.toString() ?? ''; String dbOppTeam = game['opponent_team']?.toString() ?? '';
int dbMyScore = int.tryParse(game['my_score'].toString()) ?? 0; int dbMyScore = int.tryParse(game['my_score'].toString()) ?? 0;
int dbOppScore = int.tryParse(game['opponent_score'].toString()) ?? 0; int dbOppScore = int.tryParse(game['opponent_score'].toString()) ?? 0;
String opponent; String opponent; int myScore; int oppScore;
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) { 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 { } 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 rawDate = game['game_date']?.toString() ?? '---';
String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate; String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
// Calcula Vitória, Empate ou Derrota para a equipa selecionada
String result = 'E'; String result = 'E';
if (myScore > oppScore) result = 'V'; if (myScore > oppScore) result = 'V';
if (myScore < oppScore) result = 'D'; if (myScore < oppScore) result = 'D';
return _buildGameHistoryCard( return _buildGameHistoryCard(
context: context, // Usamos o context para o sf
opponent: opponent, opponent: opponent,
result: result, result: result,
myScore: myScore, myScore: myScore,
oppScore: oppScore, oppScore: oppScore,
date: date, date: date,
sf: sf,
topPts: game['top_pts_name'] ?? '---', topPts: game['top_pts_name'] ?? '---',
topAst: game['top_ast_name'] ?? '---', topAst: game['top_ast_name'] ?? '---',
topRbs: game['top_rbs_name'] ?? '---', topRbs: game['top_rbs_name'] ?? '---',
@@ -284,7 +253,7 @@ class _HomeScreenState extends State<HomeScreen> {
}, },
), ),
SizedBox(height: 20 * sf), SizedBox(height: 20 * context.sf),
], ],
), ),
), ),
@@ -308,7 +277,7 @@ class _HomeScreenState extends State<HomeScreen> {
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)}; 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( return Card(
elevation: 4, margin: EdgeInsets.zero, elevation: 4, margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none),
@@ -329,8 +298,7 @@ class _HomeScreenState extends State<HomeScreen> {
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown, alignment: Alignment.centerLeft,
alignment: Alignment.centerLeft,
child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)), child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)),
), ),
), ),
@@ -340,8 +308,7 @@ class _HomeScreenState extends State<HomeScreen> {
Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))), Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))),
const Spacer(), const Spacer(),
Container( Container(
width: double.infinity, width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035),
padding: EdgeInsets.symmetric(vertical: ch * 0.035),
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)), 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))) 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<HomeScreen> {
} }
Widget _buildGameHistoryCard({ 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 required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp
}) { }) {
bool isWin = result == 'V'; bool isWin = result == 'V';
@@ -363,44 +330,42 @@ class _HomeScreenState extends State<HomeScreen> {
Color statusColor = isWin ? Colors.green : (isDraw ? Colors.yellow.shade700 : Colors.red); Color statusColor = isWin ? Colors.green : (isDraw ? Colors.yellow.shade700 : Colors.red);
return Container( return Container(
margin: EdgeInsets.only(bottom: 14 * sf), margin: EdgeInsets.only(bottom: 14 * context.sf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white, borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade200), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))],
border: Border.all(color: Colors.grey.shade200),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))],
), ),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: EdgeInsets.all(14 * sf), padding: EdgeInsets.all(14 * context.sf),
child: Row( child: Row(
children: [ children: [
Container( 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), 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( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(date, style: TextStyle(fontSize: 11 * sf, color: Colors.grey, fontWeight: FontWeight.w600)), Text(date, style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.w600)),
SizedBox(height: 6 * sf), SizedBox(height: 6 * context.sf),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ 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(
padding: EdgeInsets.symmetric(horizontal: 8 * sf), padding: EdgeInsets.symmetric(horizontal: 8 * context.sf),
child: Container( 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)), 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<HomeScreen> {
], ],
), ),
), ),
Divider(height: 1, color: Colors.grey.shade100, thickness: 1.5), Divider(height: 1, color: Colors.grey.shade100, thickness: 1.5),
Container( Container(
width: double.infinity, width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf),
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))),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16)),
),
child: Column( child: Column(
children: [ children: [
Row( Row(
children: [ children: [
Expanded(child: _buildGridStatRow(Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, sf, isMvp: true)), Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)),
Expanded(child: _buildGridStatRow(Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef, sf)), Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef)),
], ],
), ),
SizedBox(height: 8 * sf), SizedBox(height: 8 * context.sf),
Row( Row(
children: [ children: [
Expanded(child: _buildGridStatRow(Icons.bolt, Colors.blue.shade700, "Pontos", topPts, sf)), Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)),
Expanded(child: _buildGridStatRow(Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs, sf)), Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs)),
], ],
), ),
SizedBox(height: 8 * sf), SizedBox(height: 8 * context.sf),
Row( Row(
children: [ 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()), const Expanded(child: SizedBox()),
], ],
), ),
@@ -449,22 +408,21 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
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( return Row(
children: [ children: [
Icon(icon, size: 14 * sf, color: color), Icon(icon, size: 14 * context.sf, color: color),
SizedBox(width: 4 * sf), SizedBox(width: 4 * context.sf),
Text('$label: ', style: TextStyle(fontSize: 11 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
Expanded( Expanded(
child: Text( child: Text(
value, value,
style: TextStyle( style: TextStyle(
fontSize: 11 * sf, fontSize: 11 * context.sf,
color: isMvp ? Colors.amber.shade900 : Colors.black87, color: isMvp ? Colors.amber.shade900 : Colors.black87,
fontWeight: FontWeight.bold fontWeight: FontWeight.bold
), ),
maxLines: 1, maxLines: 1, overflow: TextOverflow.ellipsis
overflow: TextOverflow.ellipsis
) )
), ),
], ],

View File

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

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import 'dart:math' as math; import '../utils/size_extension.dart'; // 👇 A MAGIA DO SF!
class StatusPage extends StatefulWidget { class StatusPage extends StatefulWidget {
const StatusPage({super.key}); const StatusPage({super.key});
@@ -21,20 +21,17 @@ class _StatusPageState extends State<StatusPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double sf = math.min(MediaQuery.of(context).size.width, MediaQuery.of(context).size.height) / 400;
return Column( return Column(
children: [ children: [
// --- SELETOR DE EQUIPA ---
Padding( Padding(
padding: EdgeInsets.all(16.0 * sf), padding: EdgeInsets.all(16.0 * context.sf),
child: InkWell( child: InkWell(
onTap: () => _showTeamSelector(context, sf), onTap: () => _showTeamSelector(context),
child: Container( child: Container(
padding: EdgeInsets.all(12 * sf), padding: EdgeInsets.all(12 * context.sf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(15 * sf), borderRadius: BorderRadius.circular(15 * context.sf),
border: Border.all(color: Colors.grey.shade300), border: Border.all(color: Colors.grey.shade300),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)] boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)]
), ),
@@ -42,9 +39,9 @@ class _StatusPageState extends State<StatusPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row(children: [ Row(children: [
Icon(Icons.shield, color: const Color(0xFFE74C3C), size: 24 * sf), Icon(Icons.shield, color: const Color(0xFFE74C3C), size: 24 * context.sf),
SizedBox(width: 10 * sf), SizedBox(width: 10 * context.sf),
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold)) Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold))
]), ]),
const Icon(Icons.arrow_drop_down), const Icon(Icons.arrow_drop_down),
], ],
@@ -53,60 +50,39 @@ class _StatusPageState extends State<StatusPage> {
), ),
), ),
// --- TABELA DE ESTATÍSTICAS (AGORA EM TEMPO REAL) ---
Expanded( Expanded(
child: _selectedTeamId == null child: _selectedTeamId == null
? Center(child: Text("Seleciona uma equipa acima.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf))) ? Center(child: Text("Seleciona uma equipa acima.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)))
// 👇 STREAM 1: LÊ AS ESTATÍSTICAS 👇
: StreamBuilder<List<Map<String, dynamic>>>( : StreamBuilder<List<Map<String, dynamic>>>(
stream: _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!), stream: _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
builder: (context, statsSnapshot) { builder: (context, statsSnapshot) {
// 👇 STREAM 2: LÊ OS JOGOS (Para os MVPs e contagem de jogos da equipa) 👇
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: _supabase.from('games').stream(primaryKey: ['id']).eq('my_team', _selectedTeamName), stream: _supabase.from('games').stream(primaryKey: ['id']).eq('my_team', _selectedTeamName),
builder: (context, gamesSnapshot) { builder: (context, gamesSnapshot) {
return StreamBuilder<List<Map<String, dynamic>>>(
// 👇 STREAM 3: LÊ TODOS OS MEMBROS DO PLANTEL 👇 stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
// 👇 A CORREÇÃO ESTÁ AQUI: Remover o .eq('type', 'Jogador') builder: (context, membersSnapshot) {
return StreamBuilder<List<Map<String, dynamic>>>( if (statsSnapshot.connectionState == ConnectionState.waiting || gamesSnapshot.connectionState == ConnectionState.waiting || membersSnapshot.connectionState == ConnectionState.waiting) {
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 const Center(child: CircularProgressIndicator(color: Color(0xFFE74C3C))); return const Center(child: CircularProgressIndicator(color: Color(0xFFE74C3C)));
} }
final membersData = membersSnapshot.data ?? []; final membersData = membersSnapshot.data ?? [];
if (membersData.isEmpty) return Center(child: Text("Esta equipa não tem jogadores registados.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)));
if (membersData.isEmpty) {
return Center(child: Text("Esta equipa não tem jogadores registados.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf)));
}
final statsData = statsSnapshot.data ?? []; final statsData = statsSnapshot.data ?? [];
final gamesData = gamesSnapshot.data ?? []; final gamesData = gamesSnapshot.data ?? [];
// Conta o total de jogos terminados da equipa
final totalGamesPlayedByTeam = gamesData.where((g) => g['status'] == 'Terminado').length; final totalGamesPlayedByTeam = gamesData.where((g) => g['status'] == 'Terminado').length;
// Agrega os dados
final List<Map<String, dynamic>> playerTotals = _aggregateStats(statsData, gamesData, membersData); final List<Map<String, dynamic>> playerTotals = _aggregateStats(statsData, gamesData, membersData);
// Calcula os Totais da Equipa
final teamTotals = _calculateTeamTotals(playerTotals, totalGamesPlayedByTeam); final teamTotals = _calculateTeamTotals(playerTotals, totalGamesPlayedByTeam);
// Ordenação
playerTotals.sort((a, b) { playerTotals.sort((a, b) {
var valA = a[_sortColumn] ?? 0; var valA = a[_sortColumn] ?? 0;
var valB = b[_sortColumn] ?? 0; var valB = b[_sortColumn] ?? 0;
return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA); return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA);
}); });
return _buildStatsGrid(playerTotals, teamTotals, sf); return _buildStatsGrid(context, playerTotals, teamTotals);
} }
); );
} }
@@ -118,29 +94,17 @@ return StreamBuilder<List<Map<String, dynamic>>>(
); );
} }
// --- CÉREBRO CORRIGIDO ---
List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) { List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
Map<String, Map<String, dynamic>> aggregated = {}; Map<String, Map<String, dynamic>> aggregated = {};
// 1. Mete a malta toda do plantel com ZERO JOGOS e ZERO STATS
for (var member in members) { for (var member in members) {
String name = member['name']?.toString() ?? "Desconhecido"; String name = member['name']?.toString() ?? "Desconhecido";
aggregated[name] = { aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
'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) { for (var row in stats) {
String name = row['player_name']?.toString() ?? "Desconhecido"; String name = row['player_name']?.toString() ?? "Desconhecido";
if (!aggregated.containsKey(name)) aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
if (!aggregated.containsKey(name)) {
aggregated[name] = {
'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0,
};
}
aggregated[name]!['j'] += 1; aggregated[name]!['j'] += 1;
aggregated[name]!['pts'] += (row['pts'] ?? 0); aggregated[name]!['pts'] += (row['pts'] ?? 0);
@@ -150,20 +114,13 @@ return StreamBuilder<List<Map<String, dynamic>>>(
aggregated[name]!['blk'] += (row['blk'] ?? 0); aggregated[name]!['blk'] += (row['blk'] ?? 0);
} }
// 3. Conta os troféus
for (var game in games) { for (var game in games) {
String? mvp = game['mvp_name']; String? mvp = game['mvp_name'];
String? defRaw = game['top_def_name']; String? defRaw = game['top_def_name'];
if (mvp != null && aggregated.containsKey(mvp)) aggregated[mvp]!['mvp'] += 1;
if (mvp != null && aggregated.containsKey(mvp)) {
aggregated[mvp]!['mvp'] += 1;
}
if (defRaw != null) { if (defRaw != null) {
String defName = defRaw.split(' (')[0].trim(); String defName = defRaw.split(' (')[0].trim();
if (aggregated.containsKey(defName)) { if (aggregated.containsKey(defName)) aggregated[defName]!['def'] += 1;
aggregated[defName]!['def'] += 1;
}
} }
} }
return aggregated.values.toList(); return aggregated.values.toList();
@@ -172,14 +129,12 @@ return StreamBuilder<List<Map<String, dynamic>>>(
Map<String, dynamic> _calculateTeamTotals(List<Map<String, dynamic>> players, int teamGames) { Map<String, dynamic> _calculateTeamTotals(List<Map<String, dynamic>> players, int teamGames) {
int tPts = 0, tAst = 0, tRbs = 0, tStl = 0, tBlk = 0, tMvp = 0, tDef = 0; int tPts = 0, tAst = 0, tRbs = 0, tStl = 0, tBlk = 0, tMvp = 0, tDef = 0;
for (var p in players) { for (var p in players) {
tPts += (p['pts'] as int); tAst += (p['ast'] as int); tRbs += (p['rbs'] 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);
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}; return {'name': 'TOTAL EQUIPA', 'j': teamGames, 'pts': tPts, 'ast': tAst, 'rbs': tRbs, 'stl': tStl, 'blk': tBlk, 'mvp': tMvp, 'def': tDef};
} }
Widget _buildStatsGrid(List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, double sf) { Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals) {
return Container( return Container(
color: Colors.white, color: Colors.white,
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -187,49 +142,44 @@ return StreamBuilder<List<Map<String, dynamic>>>(
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: DataTable( child: DataTable(
columnSpacing: 25 * sf, columnSpacing: 25 * context.sf,
headingRowColor: MaterialStateProperty.all(Colors.grey.shade100), headingRowColor: MaterialStateProperty.all(Colors.grey.shade100),
dataRowHeight: 60 * sf, dataRowHeight: 60 * context.sf,
columns: [ columns: [
DataColumn(label: const Text('JOGADOR')), DataColumn(label: const Text('JOGADOR')),
_buildSortableColumn('J', 'j', sf), _buildSortableColumn(context, 'J', 'j'),
_buildSortableColumn('PTS', 'pts', sf), _buildSortableColumn(context, 'PTS', 'pts'),
_buildSortableColumn('AST', 'ast', sf), _buildSortableColumn(context, 'AST', 'ast'),
_buildSortableColumn('RBS', 'rbs', sf), _buildSortableColumn(context, 'RBS', 'rbs'),
_buildSortableColumn('STL', 'stl', sf), _buildSortableColumn(context, 'STL', 'stl'),
_buildSortableColumn('BLK', 'blk', sf), _buildSortableColumn(context, 'BLK', 'blk'),
_buildSortableColumn('DEF 🛡️', 'def', sf), _buildSortableColumn(context, 'DEF 🛡️', 'def'),
_buildSortableColumn('MVP 🏆', 'mvp', sf), _buildSortableColumn(context, 'MVP 🏆', 'mvp'),
], ],
rows: [ rows: [
...players.map((player) => DataRow(cells: [ ...players.map((player) => DataRow(cells: [
DataCell(Row(children: [ 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))])),
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(Center(child: Text(player['j'].toString()))), DataCell(Center(child: Text(player['j'].toString()))),
_buildStatCell(player['pts'], sf, isHighlight: true), _buildStatCell(context, player['pts'], isHighlight: true),
_buildStatCell(player['ast'], sf), _buildStatCell(context, player['ast']),
_buildStatCell(player['rbs'], sf), _buildStatCell(context, player['rbs']),
_buildStatCell(player['stl'], sf), _buildStatCell(context, player['stl']),
_buildStatCell(player['blk'], sf), _buildStatCell(context, player['blk']),
_buildStatCell(player['def'], sf, isBlue: true), _buildStatCell(context, player['def'], isBlue: true),
_buildStatCell(player['mvp'], sf, isGold: true), _buildStatCell(context, player['mvp'], isGold: true),
])), ])),
DataRow( DataRow(
color: MaterialStateProperty.all(Colors.grey.shade50), color: MaterialStateProperty.all(Colors.grey.shade50),
cells: [ 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)))), DataCell(Center(child: Text(teamTotals['j'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)))),
_buildStatCell(teamTotals['pts'], sf, isHighlight: true), _buildStatCell(context, teamTotals['pts'], isHighlight: true),
_buildStatCell(teamTotals['ast'], sf), _buildStatCell(context, teamTotals['ast']),
_buildStatCell(teamTotals['rbs'], sf), _buildStatCell(context, teamTotals['rbs']),
_buildStatCell(teamTotals['stl'], sf), _buildStatCell(context, teamTotals['stl']),
_buildStatCell(teamTotals['blk'], sf), _buildStatCell(context, teamTotals['blk']),
_buildStatCell(teamTotals['def'], sf, isBlue: true), _buildStatCell(context, teamTotals['def'], isBlue: true),
_buildStatCell(teamTotals['mvp'], sf, isGold: true), _buildStatCell(context, teamTotals['mvp'], isGold: true),
] ]
) )
], ],
@@ -239,35 +189,31 @@ return StreamBuilder<List<Map<String, dynamic>>>(
); );
} }
DataColumn _buildSortableColumn(String title, String sortKey, double sf) { DataColumn _buildSortableColumn(BuildContext context, String title, String sortKey) {
return DataColumn(label: InkWell( return DataColumn(label: InkWell(
onTap: () => setState(() { onTap: () => setState(() {
if (_sortColumn == sortKey) _isAscending = !_isAscending; if (_sortColumn == sortKey) _isAscending = !_isAscending;
else { _sortColumn = sortKey; _isAscending = false; } else { _sortColumn = sortKey; _isAscending = false; }
}), }),
child: Row(children: [ child: Row(children: [
Text(title, style: TextStyle(fontSize: 12 * sf, fontWeight: FontWeight.bold)), 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 * sf, color: const Color(0xFFE74C3C)), 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( return DataCell(Center(child: Container(
padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 4 * sf), padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
decoration: BoxDecoration( 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)),
color: isGold && value > 0 ? Colors.amber.withOpacity(0.2) : (isBlue && value > 0 ? Colors.blue.withOpacity(0.1) : Colors.transparent),
borderRadius: BorderRadius.circular(6),
),
child: Text(value == 0 ? "-" : value.toString(), style: TextStyle( child: Text(value == 0 ? "-" : value.toString(), style: TextStyle(
fontWeight: (isHighlight || isGold || isBlue) ? FontWeight.w900 : FontWeight.w600, fontWeight: (isHighlight || isGold || isBlue) ? FontWeight.w900 : FontWeight.w600,
fontSize: 14 * sf, fontSize: 14 * context.sf, color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? Colors.green.shade700 : Colors.black87))
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<List<Map<String, dynamic>>>( showModalBottomSheet(context: context, builder: (context) => StreamBuilder<List<Map<String, dynamic>>>(
stream: _teamController.teamsStream, stream: _teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:playmaker/screens/team_stats_page.dart'; import 'package:playmaker/screens/team_stats_page.dart';
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../models/team_model.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 { class TeamsPage extends StatefulWidget {
const TeamsPage({super.key}); const TeamsPage({super.key});
@@ -25,8 +25,7 @@ class _TeamsPageState extends State<TeamsPage> {
super.dispose(); super.dispose();
} }
// --- POPUP DE FILTROS --- void _showFilterDialog(BuildContext context) {
void _showFilterDialog(BuildContext context, double sf) {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
@@ -34,16 +33,13 @@ class _TeamsPageState extends State<TeamsPage> {
builder: (context, setModalState) { builder: (context, setModalState) {
return AlertDialog( return AlertDialog(
backgroundColor: const Color(0xFF2C3E50), backgroundColor: const Color(0xFF2C3E50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text("Filtros de pesquisa", style: TextStyle(color: Colors.white, fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
"Filtros de pesquisa",
style: TextStyle(color: Colors.white, fontSize: 18 * sf, fontWeight: FontWeight.bold),
),
IconButton( 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), onPressed: () => Navigator.pop(context),
) )
], ],
@@ -52,31 +48,27 @@ class _TeamsPageState extends State<TeamsPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Divider(color: Colors.white24), const Divider(color: Colors.white24),
SizedBox(height: 16 * sf), SizedBox(height: 16 * context.sf),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Coluna Temporada
Expanded( Expanded(
child: _buildPopupColumn( child: _buildPopupColumn(
title: "TEMPORADA", title: "TEMPORADA",
options: ['Todas', '2023/24', '2024/25', '2025/26'], options: ['Todas', '2023/24', '2024/25', '2025/26'],
currentValue: _selectedSeason, currentValue: _selectedSeason,
sf: sf,
onSelect: (val) { onSelect: (val) {
setState(() => _selectedSeason = val); setState(() => _selectedSeason = val);
setModalState(() {}); setModalState(() {});
}, },
), ),
), ),
SizedBox(width: 20 * sf), SizedBox(width: 20 * context.sf),
// Coluna Ordenar
Expanded( Expanded(
child: _buildPopupColumn( child: _buildPopupColumn(
title: "ORDENAR POR", title: "ORDENAR POR",
options: ['Recentes', 'Nome', 'Tamanho'], options: ['Recentes', 'Nome', 'Tamanho'],
currentValue: _currentSort, currentValue: _currentSort,
sf: sf,
onSelect: (val) { onSelect: (val) {
setState(() => _currentSort = val); setState(() => _currentSort = val);
setModalState(() {}); setModalState(() {});
@@ -90,7 +82,7 @@ class _TeamsPageState extends State<TeamsPage> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text("CONCLUÍDO", style: TextStyle(color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold, fontSize: 14 * 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<TeamsPage> {
); );
} }
Widget _buildPopupColumn({ Widget _buildPopupColumn({required String title, required List<String> options, required String currentValue, required Function(String) onSelect}) {
required String title,
required List<String> options,
required String currentValue,
required double sf,
required Function(String) onSelect,
}) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, style: TextStyle(color: Colors.grey, fontSize: 11 * sf, fontWeight: FontWeight.bold)), Text(title, style: TextStyle(color: Colors.grey, fontSize: 11 * context.sf, fontWeight: FontWeight.bold)),
SizedBox(height: 12 * sf), SizedBox(height: 12 * context.sf),
...options.map((opt) { ...options.map((opt) {
final isSelected = currentValue == opt; final isSelected = currentValue == opt;
return InkWell( return InkWell(
onTap: () => onSelect(opt), onTap: () => onSelect(opt),
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(vertical: 8.0 * sf), padding: EdgeInsets.symmetric(vertical: 8.0 * context.sf),
child: Text( child: Text(
opt, opt,
style: TextStyle( style: TextStyle(
color: isSelected ? const Color(0xFFE74C3C) : Colors.white70, color: isSelected ? const Color(0xFFE74C3C) : Colors.white70,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 14 * sf, fontSize: 14 * context.sf,
), ),
), ),
), ),
@@ -135,109 +121,84 @@ class _TeamsPageState extends State<TeamsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 👇 CÁLCULO DA ESCALA (sf) PARA SE ADAPTAR A QUALQUER ECRÃ 👇 // 🔥 OLHA QUE LIMPEZA: Já não precisamos de calcular nada aqui!
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
final double sf = math.min(wScreen, hScreen) / 400;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F7FA), backgroundColor: const Color(0xFFF5F7FA),
appBar: AppBar( 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), backgroundColor: const Color(0xFFF5F7FA),
elevation: 0, elevation: 0,
actions: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.filter_list, color: const Color(0xFFE74C3C), size: 24 * sf), icon: Icon(Icons.filter_list, color: const Color(0xFFE74C3C), size: 24 * context.sf),
onPressed: () => _showFilterDialog(context, sf), onPressed: () => _showFilterDialog(context),
), ),
], ],
), ),
body: Column( body: Column(
children: [ children: [
_buildSearchBar(sf), _buildSearchBar(),
Expanded(child: _buildTeamsList(sf)), Expanded(child: _buildTeamsList()),
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
backgroundColor: const Color(0xFFE74C3C), backgroundColor: const Color(0xFFE74C3C),
child: Icon(Icons.add, color: Colors.white, size: 24 * sf), child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
onPressed: () => _showCreateDialog(context, sf), onPressed: () => _showCreateDialog(context),
), ),
); );
} }
Widget _buildSearchBar(double sf) { Widget _buildSearchBar() {
return Padding( return Padding(
padding: EdgeInsets.all(16.0 * sf), padding: EdgeInsets.all(16.0 * context.sf),
child: TextField( child: TextField(
controller: _searchController, controller: _searchController,
onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()), onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()),
style: TextStyle(fontSize: 16 * sf), style: TextStyle(fontSize: 16 * context.sf),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Pesquisar equipa...', hintText: 'Pesquisar equipa...',
hintStyle: TextStyle(fontSize: 16 * sf), hintStyle: TextStyle(fontSize: 16 * context.sf),
prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * sf), prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * context.sf),
filled: true, filled: true,
fillColor: Colors.white, 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<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: controller.teamsStream, stream: controller.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
return const Center(child: CircularProgressIndicator()); if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * context.sf)));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * sf)));
}
var data = List<Map<String, dynamic>>.from(snapshot.data!); var data = List<Map<String, dynamic>>.from(snapshot.data!);
// --- 1. FILTROS --- if (_selectedSeason != 'Todas') data = data.where((t) => t['season'] == _selectedSeason).toList();
if (_selectedSeason != 'Todas') { if (_searchQuery.isNotEmpty) data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList();
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) { data.sort((a, b) {
bool favA = a['is_favorite'] ?? false; bool favA = a['is_favorite'] ?? false;
bool favB = b['is_favorite'] ?? false; bool favB = b['is_favorite'] ?? false;
if (favA && !favB) return -1; if (favA && !favB) return -1;
if (!favA && favB) return 1; if (!favA && favB) return 1;
if (_currentSort == 'Nome') { if (_currentSort == 'Nome') return a['name'].toString().compareTo(b['name'].toString());
return a['name'].toString().compareTo(b['name'].toString()); else return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString());
} else {
return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString());
}
}); });
return ListView.builder( return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16 * sf), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf),
itemCount: data.length, itemCount: data.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final team = Team.fromMap(data[index]); final team = Team.fromMap(data[index]);
return GestureDetector( return GestureDetector(
onTap: () { onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))),
Navigator.push(
context,
MaterialPageRoute(builder: (context) => TeamStatsPage(team: team)),
);
},
child: TeamCard( child: TeamCard(
team: team, team: team,
controller: controller, controller: controller,
sf: sf, // Passar a escala para o Card
onFavoriteTap: () => controller.toggleFavorite(team.id, team.isFavorite), onFavoriteTap: () => controller.toggleFavorite(team.id, team.isFavorite),
), ),
); );
@@ -247,14 +208,8 @@ class _TeamsPageState extends State<TeamsPage> {
); );
} }
void _showCreateDialog(BuildContext context, double sf) { void _showCreateDialog(BuildContext context) {
showDialog( showDialog(context: context, builder: (context) => CreateTeamDialog(onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl)));
context: context,
builder: (context) => CreateTeamDialog(
sf: sf,
onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl),
),
);
} }
} }
@@ -263,129 +218,58 @@ class TeamCard extends StatelessWidget {
final Team team; final Team team;
final TeamController controller; final TeamController controller;
final VoidCallback onFavoriteTap; final VoidCallback onFavoriteTap;
final double sf; // <-- Variável de escala
const TeamCard({ const TeamCard({super.key, required this.team, required this.controller, required this.onFavoriteTap});
super.key,
required this.team,
required this.controller,
required this.onFavoriteTap,
required this.sf,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
color: Colors.white, color: Colors.white, elevation: 3, margin: EdgeInsets.only(bottom: 12 * context.sf),
elevation: 3, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
margin: EdgeInsets.only(bottom: 12 * sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)),
child: ListTile( child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf), contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 8 * context.sf),
// --- 1. IMAGEM + FAVORITO ---
leading: Stack( leading: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 28 * sf, radius: 28 * context.sf, backgroundColor: Colors.grey[200],
backgroundColor: Colors.grey[200], backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http')) ? NetworkImage(team.imageUrl) : null,
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http')) child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http')) ? Text(team.imageUrl.isEmpty ? "🏀" : team.imageUrl, style: TextStyle(fontSize: 24 * context.sf)) : null,
? NetworkImage(team.imageUrl)
: null,
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
? Text(
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
style: TextStyle(fontSize: 24 * sf),
)
: null,
), ),
Positioned( Positioned(
left: -15 * sf, left: -15 * context.sf, top: -10 * context.sf,
top: -10 * sf,
child: IconButton( child: IconButton(
icon: Icon( 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)]),
team.isFavorite ? Icons.star : Icons.star_border,
color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1),
size: 28 * sf,
shadows: [
Shadow(
color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1),
blurRadius: 4 * sf,
),
],
),
onPressed: onFavoriteTap, onPressed: onFavoriteTap,
), ),
), ),
], ],
), ),
title: Text(team.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * context.sf), overflow: TextOverflow.ellipsis),
// --- 2. TÍTULO ---
title: Text(
team.name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf),
overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos
),
// --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) ---
subtitle: Padding( subtitle: Padding(
padding: EdgeInsets.only(top: 6.0 * sf), padding: EdgeInsets.only(top: 6.0 * context.sf),
child: Row( child: Row(
children: [ children: [
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey), Icon(Icons.groups_outlined, size: 16 * context.sf, color: Colors.grey),
SizedBox(width: 4 * sf), SizedBox(width: 4 * context.sf),
// 👇 STREAMBUILDER EM VEZ DE FUTUREBUILDER 👇
StreamBuilder<int>( StreamBuilder<int>(
stream: controller.getPlayerCountStream(team.id), stream: controller.getPlayerCountStream(team.id),
initialData: 0, initialData: 0,
builder: (context, snapshot) { builder: (context, snapshot) {
final count = snapshot.data ?? 0; final count = snapshot.data ?? 0;
return Text( return Text("$count Jogs.", style: TextStyle(color: count > 0 ? Colors.green[700] : Colors.orange, fontWeight: FontWeight.bold, fontSize: 13 * context.sf));
"$count Jogs.",
style: TextStyle(
color: count > 0 ? Colors.green[700] : Colors.orange,
fontWeight: FontWeight.bold,
fontSize: 13 * sf,
),
);
}, },
), ),
SizedBox(width: 8 * context.sf),
SizedBox(width: 8 * sf), Expanded(child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * context.sf), overflow: TextOverflow.ellipsis)),
Expanded(
child: Text(
"| ${team.season}",
style: TextStyle(color: Colors.grey, fontSize: 13 * sf),
overflow: TextOverflow.ellipsis,
),
),
], ],
), ),
), ),
// --- 4. BOTÕES (Estatísticas e Apagar) ---
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, // <-- ISTO RESOLVE O OVERFLOW DAS RISCAS AMARELAS mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( 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)))),
tooltip: 'Ver Estatísticas', IconButton(tooltip: 'Eliminar Equipa', icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * context.sf), onPressed: () => _confirmDelete(context)),
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),
),
], ],
), ),
), ),
@@ -396,20 +280,11 @@ class TeamCard extends StatelessWidget {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold)), 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 * sf)), content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * context.sf)),
actions: [ actions: [
TextButton( TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))),
onPressed: () => Navigator.pop(context), TextButton(onPressed: () { controller.deleteTeam(team.id); Navigator.pop(context); }, child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * context.sf))),
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)),
),
], ],
), ),
); );
@@ -419,9 +294,7 @@ class TeamCard extends StatelessWidget {
// --- DIALOG DE CRIAÇÃO --- // --- DIALOG DE CRIAÇÃO ---
class CreateTeamDialog extends StatefulWidget { class CreateTeamDialog extends StatefulWidget {
final Function(String name, String season, String imageUrl) onConfirm; final Function(String name, String season, String imageUrl) onConfirm;
final double sf; // Recebe a escala const CreateTeamDialog({super.key, required this.onConfirm});
const CreateTeamDialog({super.key, required this.onConfirm, required this.sf});
@override @override
State<CreateTeamDialog> createState() => _CreateTeamDialogState(); State<CreateTeamDialog> createState() => _CreateTeamDialogState();
@@ -435,69 +308,31 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold)), title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField( TextField(controller: _nameController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * context.sf)), textCapitalization: TextCapitalization.words),
controller: _nameController, SizedBox(height: 15 * context.sf),
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),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: _selectedSeason, value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf)),
decoration: InputDecoration( style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87),
labelText: 'Temporada', items: ['2023/24', '2024/25', '2025/26'].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
labelStyle: TextStyle(fontSize: 14 * widget.sf)
),
style: TextStyle(fontSize: 14 * widget.sf, color: Colors.black87),
items: ['2023/24', '2024/25', '2025/26']
.map((s) => DropdownMenuItem(value: s, child: Text(s)))
.toList(),
onChanged: (val) => setState(() => _selectedSeason = val!), onChanged: (val) => setState(() => _selectedSeason = val!),
), ),
SizedBox(height: 15 * widget.sf), SizedBox(height: 15 * context.sf),
TextField( 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))),
controller: _imageController,
style: TextStyle(fontSize: 14 * widget.sf),
decoration: InputDecoration(
labelText: 'URL Imagem ou Emoji',
labelStyle: TextStyle(fontSize: 14 * widget.sf),
hintText: 'Ex: 🏀 ou https://...',
hintStyle: TextStyle(fontSize: 14 * widget.sf)
),
),
], ],
), ),
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))),
onPressed: () => Navigator.pop(context),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf))
),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)),
backgroundColor: const Color(0xFFE74C3C), onPressed: () { if (_nameController.text.trim().isNotEmpty) { widget.onConfirm(_nameController.text.trim(), _selectedSeason, _imageController.text.trim()); Navigator.pop(context); } },
padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf) child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * context.sf)),
),
onPressed: () {
if (_nameController.text.trim().isNotEmpty) {
widget.onConfirm(
_nameController.text.trim(),
_selectedSeason,
_imageController.text.trim(),
);
Navigator.pop(context);
}
},
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)),
), ),
], ],
); );

View File

@@ -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;
}
}

View File

@@ -1,194 +1,146 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/controllers/login_controller.dart'; import 'package:playmaker/controllers/login_controller.dart';
import 'package:playmaker/pages/RegisterPage.dart'; import 'package:playmaker/pages/RegisterPage.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
class BasketTrackHeader extends StatelessWidget { class BasketTrackHeader extends StatelessWidget {
const BasketTrackHeader({super.key}); 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});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width; return Column(
final verticalPadding = screenWidth > 600 ? 22.0 : 16.0; 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( return Column(
children: [ children: [
TextField( TextField(
controller: controller.emailController, controller: controller.emailController,
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'E-mail', labelText: 'E-mail',
prefixIcon: const Icon(Icons.email_outlined), labelStyle: TextStyle(fontSize: 15 * context.sf),
// O erro agora vem diretamente do controller prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf),
errorText: controller.emailError, errorText: controller.emailError,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16), contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
), ),
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
), ),
const SizedBox(height: 20), SizedBox(height: 20 * context.sf),
TextField( TextField(
controller: controller.passwordController, controller: controller.passwordController,
obscureText: controller.obscurePassword, obscureText: controller.obscurePassword,
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Palavra-passe', 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, errorText: controller.passwordError,
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon(controller.obscurePassword icon: Icon(
? Icons.visibility_outlined controller.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
: Icons.visibility_off_outlined), size: 22 * context.sf
),
onPressed: controller.togglePasswordVisibility, onPressed: controller.togglePasswordVisibility,
), ),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16), contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
), ),
), ),
], ],
); );
} }
} }
class LoginButton extends StatelessWidget {
final LoginController controller;
final VoidCallback onLoginSuccess;
const LoginButton({ class LoginButton extends StatelessWidget {
super.key, final LoginController controller;
required this.controller, final VoidCallback onLoginSuccess;
required this.onLoginSuccess,
});
@override const LoginButton({super.key, required this.controller, required this.onLoginSuccess});
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
return SizedBox( @override
width: double.infinity, Widget build(BuildContext context) {
height: buttonHeight, return SizedBox(
child: ElevatedButton( width: double.infinity,
onPressed: controller.isLoading ? null : () async { height: 58 * context.sf,
final success = await controller.login(); child: ElevatedButton(
if (success) { onPressed: controller.isLoading ? null : () async {
onLoginSuccess(); final success = await controller.login();
} if (success) onLoginSuccess();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C), backgroundColor: const Color(0xFFE74C3C),
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
borderRadius: BorderRadius.circular(14), // ↑ Bordas mais arredondadas elevation: 3,
),
elevation: 3, // ↑ Sombra mais pronunciada
),
child: controller.isLoading
? SizedBox(
width: 28, // ↑ Aumentado
height: 28, // ↑ Aumentado
child: CircularProgressIndicator(
strokeWidth: 3, // ↑ Aumentado
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'Entrar',
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w700, //
),
),
), ),
); child: controller.isLoading
} ? SizedBox(
width: 28 * context.sf, height: 28 * context.sf,
child: const CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(Colors.white)),
)
: Text('Entrar', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
),
);
} }
}
class CreateAccountButton extends StatelessWidget { class CreateAccountButton extends StatelessWidget {
const CreateAccountButton({super.key}); const CreateAccountButton({super.key});
@override @override
Widget build(BuildContext context) { 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( return SizedBox(
width: double.infinity, width: double.infinity,
height: buttonHeight, height: 58 * context.sf,
child: OutlinedButton( child: OutlinedButton(
onPressed: () { 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( style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFE74C3C), foregroundColor: const Color(0xFFE74C3C),
side: const BorderSide(color: Color(0xFFE74C3C), width: 2), side: BorderSide(color: const Color(0xFFE74C3C), width: 2 * context.sf),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
borderRadius: BorderRadius.circular(14),
),
),
child: Text(
'Criar Conta',
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w700,
),
), ),
child: Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
), ),
); );
} }
} }

View File

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

View File

@@ -61,10 +61,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.0"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -268,18 +268,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.18" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.0" version: "0.11.1"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -553,10 +553,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.9" version: "0.7.7"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View File

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