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/services.dart';
import 'package:playmaker/controllers/placar_controller.dart';
import 'package:playmaker/widgets/placar_widgets.dart';
import 'dart:math' as math;
import '../utils/size_extension.dart'; // 👇 EXTENSÃO IMPORTADA
class PlacarPage extends StatefulWidget {
final String gameId, myTeam, opponentTeam;
@@ -42,30 +41,20 @@ class _PlacarPageState extends State<PlacarPage> {
}
// --- BOTÕES FLUTUANTES DE FALTA ---
Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top, double sf) {
Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top) {
return Positioned(
top: top,
left: left > 0 ? left : null,
right: right > 0 ? right : null,
top: top, left: left > 0 ? left : null, right: right > 0 ? right : null,
child: Draggable<String>(
data: action,
feedback: Material(
color: Colors.transparent,
child: CircleAvatar(
radius: 30 * sf,
backgroundColor: color.withOpacity(0.8),
child: Icon(icon, color: Colors.white, size: 30 * sf)
),
child: CircleAvatar(radius: 30 * context.sf, backgroundColor: color.withOpacity(0.8), child: Icon(icon, color: Colors.white, size: 30 * context.sf)),
),
child: Column(
children: [
CircleAvatar(
radius: 27 * sf,
backgroundColor: color,
child: Icon(icon, color: Colors.white, size: 28 * sf),
),
SizedBox(height: 5 * sf),
Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 * sf)),
CircleAvatar(radius: 27 * context.sf, backgroundColor: color, child: Icon(icon, color: Colors.white, size: 28 * context.sf)),
SizedBox(height: 5 * context.sf),
Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 * context.sf)),
],
),
),
@@ -75,16 +64,13 @@ class _PlacarPageState extends State<PlacarPage> {
// --- BOTÕES LATERAIS QUADRADOS ---
Widget _buildCornerBtn({required String heroTag, required IconData icon, required Color color, required VoidCallback onTap, required double size, bool isLoading = false}) {
return SizedBox(
width: size,
height: size,
width: size, height: size,
child: FloatingActionButton(
heroTag: heroTag,
backgroundColor: color,
heroTag: heroTag, backgroundColor: color,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * (size / 50))),
elevation: 5,
onPressed: isLoading ? null : onTap,
elevation: 5, onPressed: isLoading ? null : onTap,
child: isLoading
? SizedBox(width: size*0.45, height: size*0.45, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5))
? SizedBox(width: size*0.45, height: size*0.45, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5))
: Icon(icon, color: Colors.white, size: size * 0.55),
),
);
@@ -95,10 +81,7 @@ class _PlacarPageState extends State<PlacarPage> {
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
// 👇 DIVISOR AUMENTADO PARA O 'sf' FICAR MAIS PEQUENO 👇
final double sf = math.min(wScreen / 1150, hScreen / 720);
final double cornerBtnSize = 48 * sf; // Tamanho ideal (Nem 38 nem 55)
final double cornerBtnSize = 48 * context.sf;
if (_controller.isLoading) {
return Scaffold(
@@ -107,20 +90,18 @@ class _PlacarPageState extends State<PlacarPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("PREPARANDO O PAVILHÃO", style: TextStyle(color: Colors.white24, fontSize: 45 * sf, fontWeight: FontWeight.bold, letterSpacing: 2)),
SizedBox(height: 35 * sf),
Text("PREPARANDO O PAVILHÃO", style: TextStyle(color: Colors.white24, fontSize: 45 * context.sf, fontWeight: FontWeight.bold, letterSpacing: 2)),
SizedBox(height: 35 * context.sf),
StreamBuilder(
stream: Stream.periodic(const Duration(seconds: 3)),
builder: (context, snapshot) {
List<String> frases = [
"O Treinador está a desenhar a tática...",
"A encher as bolas com ar de campeão...",
"O árbitro está a testar o apito...",
"A verificar se o cesto está nivelado...",
"O Treinador está a desenhar a tática...", "A encher as bolas com ar de campeão...",
"O árbitro está a testar o apito...", "A verificar se o cesto está nivelado...",
"Os jogadores estão a terminar o aquecimento..."
];
String frase = frases[DateTime.now().second % frases.length];
return Text(frase, style: TextStyle(color: Colors.orange.withOpacity(0.7), fontSize: 26 * sf, fontStyle: FontStyle.italic));
return Text(frase, style: TextStyle(color: Colors.orange.withOpacity(0.7), fontSize: 26 * context.sf, fontStyle: FontStyle.italic));
},
),
],
@@ -132,13 +113,12 @@ class _PlacarPageState extends State<PlacarPage> {
return Scaffold(
backgroundColor: const Color(0xFF266174),
body: SafeArea(
top: false,
bottom: false,
top: false, bottom: false,
child: Stack(
children: [
// --- O CAMPO ---
Container(
margin: EdgeInsets.only(left: 65 * sf, right: 65 * sf, bottom: 55 * sf),
margin: EdgeInsets.only(left: 65 * context.sf, right: 65 * context.sf, bottom: 55 * context.sf),
decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.5)),
child: LayoutBuilder(
builder: (context, constraints) {
@@ -155,14 +135,12 @@ class _PlacarPageState extends State<PlacarPage> {
},
child: Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/campo.png'),
fit: BoxFit.fill, // <-- A MÁGICA ESTÁ AQUI
), ),
image: DecorationImage(image: AssetImage('assets/campo.png'), fit: BoxFit.fill),
),
child: Stack(
children: _controller.matchShots.map((shot) => Positioned(
left: shot.position.dx - (9 * sf), top: shot.position.dy - (9 * sf),
child: CircleAvatar(radius: 9 * sf, backgroundColor: shot.isMake ? Colors.green : Colors.red, child: Icon(shot.isMake ? Icons.check : Icons.close, size: 11 * sf, color: Colors.white)),
left: shot.position.dx - (9 * context.sf), top: shot.position.dy - (9 * context.sf),
child: CircleAvatar(radius: 9 * context.sf, backgroundColor: shot.isMake ? Colors.green : Colors.red, child: Icon(shot.isMake ? Icons.check : Icons.close, size: 11 * context.sf, color: Colors.white)),
)).toList(),
),
),
@@ -170,47 +148,47 @@ image: DecorationImage(
// --- JOGADORES ---
if (!_controller.isSelectingShotLocation) ...[
Positioned(top: h * 0.25, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[0], isOpponent: false, sf: sf)),
Positioned(top: h * 0.68, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[1], isOpponent: false, sf: sf)),
Positioned(top: h * 0.45, left: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[2], isOpponent: false, sf: sf)),
Positioned(top: h * 0.15, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[3], isOpponent: false, sf: sf)),
Positioned(top: h * 0.80, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[4], isOpponent: false, sf: sf)),
Positioned(top: h * 0.25, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[0], isOpponent: false)),
Positioned(top: h * 0.68, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[1], isOpponent: false)),
Positioned(top: h * 0.45, left: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[2], isOpponent: false)),
Positioned(top: h * 0.15, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[3], isOpponent: false)),
Positioned(top: h * 0.80, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[4], isOpponent: false)),
Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[0], isOpponent: true, sf: sf)),
Positioned(top: h * 0.68, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[1], isOpponent: true, sf: sf)),
Positioned(top: h * 0.45, right: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[2], isOpponent: true, sf: sf)),
Positioned(top: h * 0.15, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[3], isOpponent: true, sf: sf)),
Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[4], isOpponent: true, sf: sf)),
Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[0], isOpponent: true)),
Positioned(top: h * 0.68, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[1], isOpponent: true)),
Positioned(top: h * 0.45, right: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[2], isOpponent: true)),
Positioned(top: h * 0.15, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[3], isOpponent: true)),
Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[4], isOpponent: true)),
],
// --- BOTÕES DE FALTAS ---
if (!_controller.isSelectingShotLocation) ...[
_buildFloatingFoulBtn("FALTA +", Colors.orange, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31, sf),
_buildFloatingFoulBtn("FALTA -", Colors.redAccent, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31, sf),
_buildFloatingFoulBtn("FALTA +", Colors.orange, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31),
_buildFloatingFoulBtn("FALTA -", Colors.redAccent, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31),
],
// --- BOTÃO PLAY/PAUSE ---
if (!_controller.isSelectingShotLocation)
Positioned(
if (!_controller.isSelectingShotLocation)
Positioned(
top: (h * 0.32) + (40 * context.sf),
left: 0, right: 0,
child: Center(
child: GestureDetector(
onTap: () => _controller.toggleTimer(context),
child: CircleAvatar(
radius: 68 * context.sf,
backgroundColor: Colors.grey.withOpacity(0.5),
child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 58 * context.sf)
),
),
),
),
top: (h * 0.32) + (40 * sf),
left: 0, right: 0,
child: Center(
child: GestureDetector(
onTap: () => _controller.toggleTimer(context),
child: CircleAvatar(
radius: 68 * sf,
backgroundColor: Colors.grey.withOpacity(0.5),
child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 58 * sf)
),
),
),
),
// --- PLACAR NO TOPO ---
Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))),
Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller))),
// --- BOTÕES DE AÇÃO ---
if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller, sf: sf)),
if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * context.sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller)),
// --- OVERLAY LANÇAMENTO ---
if (_controller.isSelectingShotLocation)
@@ -218,9 +196,9 @@ image: DecorationImage(
top: h * 0.4, left: 0, right: 0,
child: Center(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 35 * sf, vertical: 18 * sf),
decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(11 * sf), border: Border.all(color: Colors.white, width: 1.5 * sf)),
child: Text("TOQUE NO CAMPO PARA MARCAR O LOCAL DO LANÇAMENTO", style: TextStyle(color: Colors.white, fontSize: 27 * sf, fontWeight: FontWeight.bold)),
padding: EdgeInsets.symmetric(horizontal: 35 * context.sf, vertical: 18 * context.sf),
decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(11 * context.sf), border: Border.all(color: Colors.white, width: 1.5 * context.sf)),
child: Text("TOQUE NO CAMPO PARA MARCAR O LOCAL DO LANÇAMENTO", style: TextStyle(color: Colors.white, fontSize: 27 * context.sf, fontWeight: FontWeight.bold)),
),
),
),
@@ -231,75 +209,378 @@ image: DecorationImage(
),
// --- BOTÕES LATERAIS ---
// Topo Esquerdo: Guardar e Sair (Botão Único)
Positioned(
top: 50 * sf, left: 12 * sf,
child: _buildCornerBtn(
heroTag: 'btn_save_exit',
icon: Icons.save_alt, // Mudei o ícone para dar a ideia de "Guardar e Sair"
color: const Color(0xFFD92C2C), // Mantive vermelho para saberes que é para fechar
size: cornerBtnSize,
isLoading: _controller.isSaving,
onTap: () async {
// 1. Primeiro obriga a guardar os dados na BD
await _controller.saveGameStats(context);
// 2. Só depois de acabar de guardar é que volta para trás (sai da página)
if (context.mounted) {
Navigator.pop(context);
}
}
),
),
// Base Esquerda: Banco Casa + TIMEOUT DA CASA
Positioned(
bottom: 55 * sf, left: 12 * sf,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_controller.showMyBench) BenchPlayersList(controller: _controller, isOpponent: false, sf: sf),
SizedBox(height: 12 * sf),
_buildCornerBtn(heroTag: 'btn_sub_home', icon: Icons.swap_horiz, color: const Color(0xFF1E5BB2), size: cornerBtnSize, onTap: () { _controller.showMyBench = !_controller.showMyBench; _controller.onUpdate(); }),
SizedBox(height: 12 * sf),
_buildCornerBtn(
heroTag: 'btn_to_home',
icon: Icons.timer,
color: _controller.myTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFF1E5BB2),
size: cornerBtnSize,
onTap: _controller.myTimeoutsUsed >= 3
? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 A equipa da casa já usou os 3 Timeouts deste período!'), backgroundColor: Colors.red))
: () => _controller.useTimeout(false)
),
],
),
Positioned(
top: 50 * context.sf, left: 12 * context.sf,
child: _buildCornerBtn(
heroTag: 'btn_save_exit', icon: Icons.save_alt, color: const Color(0xFFD92C2C), size: cornerBtnSize, isLoading: _controller.isSaving,
onTap: () async {
await _controller.saveGameStats(context);
if (context.mounted) Navigator.pop(context);
}
),
),
// Base Direita: Banco Visitante + TIMEOUT DO VISITANTE
Positioned(
bottom: 55 * sf, right: 12 * sf,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_controller.showOppBench) BenchPlayersList(controller: _controller, isOpponent: true, sf: sf),
SizedBox(height: 12 * sf),
_buildCornerBtn(heroTag: 'btn_sub_away', icon: Icons.swap_horiz, color: const Color(0xFFD92C2C), size: cornerBtnSize, onTap: () { _controller.showOppBench = !_controller.showOppBench; _controller.onUpdate(); }),
SizedBox(height: 12 * sf),
_buildCornerBtn(
heroTag: 'btn_to_away',
icon: Icons.timer,
color: _controller.opponentTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFFD92C2C),
size: cornerBtnSize,
onTap: _controller.opponentTimeoutsUsed >= 3
? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 A equipa visitante já usou os 3 Timeouts deste período!'), backgroundColor: Colors.red))
: () => _controller.useTimeout(true)
),
],
),
// Base Esquerda: Banco Casa + TIMEOUT DA CASA
Positioned(
bottom: 55 * context.sf, left: 12 * context.sf,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_controller.showMyBench) BenchPlayersList(controller: _controller, isOpponent: false),
SizedBox(height: 12 * context.sf),
_buildCornerBtn(heroTag: 'btn_sub_home', icon: Icons.swap_horiz, color: const Color(0xFF1E5BB2), size: cornerBtnSize, onTap: () { _controller.showMyBench = !_controller.showMyBench; _controller.onUpdate(); }),
SizedBox(height: 12 * context.sf),
_buildCornerBtn(
heroTag: 'btn_to_home', icon: Icons.timer, color: _controller.myTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFF1E5BB2), size: cornerBtnSize,
onTap: _controller.myTimeoutsUsed >= 3 ? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 A equipa da casa já usou os 3 Timeouts deste período!'), backgroundColor: Colors.red)) : () => _controller.useTimeout(false)
),
],
),
],
),
// Base Direita: Banco Visitante + TIMEOUT DO VISITANTE
Positioned(
bottom: 55 * context.sf, right: 12 * context.sf,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_controller.showOppBench) BenchPlayersList(controller: _controller, isOpponent: true),
SizedBox(height: 12 * context.sf),
_buildCornerBtn(heroTag: 'btn_sub_away', icon: Icons.swap_horiz, color: const Color(0xFFD92C2C), size: cornerBtnSize, onTap: () { _controller.showOppBench = !_controller.showOppBench; _controller.onUpdate(); }),
SizedBox(height: 12 * context.sf),
_buildCornerBtn(
heroTag: 'btn_to_away', icon: Icons.timer, color: _controller.opponentTimeoutsUsed >= 3 ? Colors.grey : const Color(0xFFD92C2C), size: cornerBtnSize,
onTap: _controller.opponentTimeoutsUsed >= 3 ? () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 A equipa visitante já usou os 3 Timeouts deste período!'), backgroundColor: Colors.red)) : () => _controller.useTimeout(true)
),
],
),
),
],
),
),
);
}
}
// ==========================================
// WIDGETS DO PLACAR (Sem receber o `sf`)
// ==========================================
class TopScoreboard extends StatelessWidget {
final PlacarController controller;
const TopScoreboard({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 10 * context.sf, horizontal: 35 * context.sf),
decoration: BoxDecoration(
color: const Color(0xFF16202C),
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(22 * context.sf), bottomRight: Radius.circular(22 * context.sf)),
border: Border.all(color: Colors.white, width: 2.5 * context.sf),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildTeamSection(context, controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false),
SizedBox(width: 30 * context.sf),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 18 * context.sf, vertical: 5 * context.sf),
decoration: BoxDecoration(color: const Color(0xFF2C3E50), borderRadius: BorderRadius.circular(9 * context.sf)),
child: Text(controller.formatTime(), style: TextStyle(color: Colors.white, fontSize: 28 * context.sf, fontWeight: FontWeight.w900, fontFamily: 'monospace', letterSpacing: 2 * context.sf)),
),
SizedBox(height: 5 * context.sf),
Text("PERÍODO ${controller.currentQuarter}", style: TextStyle(color: Colors.orangeAccent, fontSize: 14 * context.sf, fontWeight: FontWeight.w900)),
],
),
SizedBox(width: 30 * context.sf),
_buildTeamSection(context, controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true),
],
),
);
}
Widget _buildTeamSection(BuildContext context, String name, int score, int fouls, int timeouts, Color color, bool isOpp) {
int displayFouls = fouls > 5 ? 5 : fouls;
final timeoutIndicators = Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (index) => Container(
margin: EdgeInsets.symmetric(horizontal: 3.5 * context.sf),
width: 12 * context.sf, height: 12 * context.sf,
decoration: BoxDecoration(shape: BoxShape.circle, color: index < timeouts ? Colors.yellow : Colors.grey.shade600, border: Border.all(color: Colors.white54, width: 1.5 * context.sf)),
)),
);
List<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 '../controllers/register_controller.dart';
import '../widgets/register_widgets.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
class RegisterPage extends StatefulWidget {
const RegisterPage({super.key});
@@ -10,39 +11,44 @@ class RegisterPage extends StatefulWidget {
}
class _RegisterPageState extends State<RegisterPage> {
// Instancia o controller
final RegisterController _controller = RegisterController();
@override
void dispose() {
_controller.dispose(); // Limpa a memória ao sair
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Criar Conta")),
backgroundColor: Colors.white,
appBar: AppBar(
title: Text("Criar Conta", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
backgroundColor: Colors.white,
elevation: 0,
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
padding: EdgeInsets.all(24.0 * context.sf),
child: ListenableBuilder(
listenable: _controller, // Ouve as mudanças (loading)
listenable: _controller,
builder: (context, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Junta-te à Equipa!",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 30),
// Widgets Extraídos
RegisterFormFields(controller: _controller),
const SizedBox(height: 24),
RegisterButton(controller: _controller),
],
return Container(
width: double.infinity,
constraints: BoxConstraints(maxWidth: 450 * context.sf), // Mesma largura do Login
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const RegisterHeader(), // 🔥 Agora sim, usa o Header bonito!
SizedBox(height: 30 * context.sf),
RegisterFormFields(controller: _controller),
SizedBox(height: 24 * context.sf),
RegisterButton(controller: _controller),
],
),
);
},
),

View File

@@ -3,114 +3,83 @@ import 'package:playmaker/pages/PlacarPage.dart';
import '../controllers/game_controller.dart';
import '../controllers/team_controller.dart';
import '../models/game_model.dart';
import 'dart:math' as math;
import '../utils/size_extension.dart'; // 👇 NOVO SUPERPODER AQUI TAMBÉM!
// --- CARD DE EXIBIÇÃO DO JOGO ---
class GameResultCard extends StatelessWidget {
final String gameId;
final String myTeam, opponentTeam, myScore, opponentScore, status, season;
final String? myTeamLogo;
final String? opponentTeamLogo;
final double sf;
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
final String? myTeamLogo, opponentTeamLogo;
const GameResultCard({
super.key,
required this.gameId,
required this.myTeam,
required this.opponentTeam,
required this.myScore,
required this.opponentScore,
required this.status,
required this.season,
this.myTeamLogo,
this.opponentTeamLogo,
required this.sf,
super.key, required this.gameId, required this.myTeam, required this.opponentTeam,
required this.myScore, required this.opponentScore, required this.status, required this.season,
this.myTeamLogo, this.opponentTeamLogo,
});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 16 * sf),
padding: EdgeInsets.all(16 * sf),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20 * sf),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)],
),
margin: EdgeInsets.only(bottom: 16 * context.sf),
padding: EdgeInsets.all(16 * context.sf),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * context.sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * context.sf)]),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, sf)),
_buildScoreCenter(context, gameId, sf),
Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, sf)),
Expanded(child: _buildTeamInfo(context, myTeam, const Color(0xFFE74C3C), myTeamLogo)),
_buildScoreCenter(context, gameId),
Expanded(child: _buildTeamInfo(context, opponentTeam, Colors.black87, opponentTeamLogo)),
],
),
);
}
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf) {
Widget _buildTeamInfo(BuildContext context, String name, Color color, String? logoUrl) {
return Column(
children: [
CircleAvatar(
radius: 24 * sf,
backgroundColor: color,
backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null,
child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * sf) : null,
),
SizedBox(height: 6 * sf),
Text(name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf),
textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2,
),
CircleAvatar(radius: 24 * context.sf, backgroundColor: color, backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * context.sf) : null),
SizedBox(height: 6 * context.sf),
Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2),
],
);
}
Widget _buildScoreCenter(BuildContext context, String id, double sf) {
Widget _buildScoreCenter(BuildContext context, String id) {
return Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
_scoreBox(myScore, Colors.green, sf),
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf)),
_scoreBox(opponentScore, Colors.grey, sf),
_scoreBox(context, myScore, Colors.green),
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * context.sf)),
_scoreBox(context, opponentScore, Colors.grey),
],
),
SizedBox(height: 10 * sf),
SizedBox(height: 10 * context.sf),
TextButton.icon(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam)));
},
icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: const Color(0xFFE74C3C)),
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
style: TextButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1),
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)),
visualDensity: VisualDensity.compact,
),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))),
icon: Icon(Icons.play_circle_fill, size: 18 * context.sf, color: const Color(0xFFE74C3C)),
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * context.sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
style: TextButton.styleFrom(backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), padding: EdgeInsets.symmetric(horizontal: 14 * context.sf, vertical: 8 * context.sf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), visualDensity: VisualDensity.compact),
),
SizedBox(height: 6 * sf),
Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * context.sf),
Text(status, style: TextStyle(fontSize: 12 * context.sf, color: Colors.blue, fontWeight: FontWeight.bold)),
],
);
}
Widget _scoreBox(String pts, Color c, double sf) => Container(
padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)),
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
Widget _scoreBox(BuildContext context, String pts, Color c) => Container(
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf, vertical: 6 * context.sf),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * context.sf)),
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)),
);
}
// --- POPUP DE CRIAÇÃO ---
class CreateGameDialogManual extends StatefulWidget {
final TeamController teamController;
final GameController gameController;
final double sf;
final GameController gameController;
const CreateGameDialogManual({super.key, required this.teamController, required this.gameController, required this.sf});
const CreateGameDialogManual({super.key, required this.teamController, required this.gameController});
@override
State<CreateGameDialogManual> createState() => _CreateGameDialogManualState();
@@ -136,27 +105,24 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * widget.sf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * widget.sf)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _seasonController, style: TextStyle(fontSize: 14 * widget.sf),
decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * widget.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * widget.sf)),
),
SizedBox(height: 15 * widget.sf),
_buildSearch(label: "Minha Equipa", controller: _myTeamController, sf: widget.sf),
Padding(padding: EdgeInsets.symmetric(vertical: 10 * widget.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * widget.sf))),
_buildSearch(label: "Adversário", controller: _opponentController, sf: widget.sf),
TextField(controller: _seasonController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * context.sf))),
SizedBox(height: 15 * context.sf),
_buildSearch(context, "Minha Equipa", _myTeamController),
Padding(padding: EdgeInsets.symmetric(vertical: 10 * context.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * context.sf))),
_buildSearch(context, "Adversário", _opponentController),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf))),
TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * context.sf))),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * widget.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)),
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)),
onPressed: _isLoading ? null : () async {
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
setState(() => _isLoading = true);
@@ -168,13 +134,13 @@ class _CreateGameDialogManualState extends State<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>>>(
stream: widget.teamController.teamsStream,
builder: (context, snapshot) {
@@ -190,9 +156,9 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0, borderRadius: BorderRadius.circular(8 * sf),
elevation: 4.0, borderRadius: BorderRadius.circular(8 * context.sf),
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 250 * sf, maxWidth: MediaQuery.of(context).size.width * 0.7),
constraints: BoxConstraints(maxHeight: 250 * context.sf, maxWidth: MediaQuery.of(context).size.width * 0.7),
child: ListView.builder(
padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
@@ -200,8 +166,8 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
final String name = option['name'].toString();
final String? imageUrl = option['image_url'];
return ListTile(
leading: CircleAvatar(radius: 20 * sf, backgroundColor: Colors.grey.shade200, backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * sf) : null),
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * sf)),
leading: CircleAvatar(radius: 20 * context.sf, backgroundColor: Colors.grey.shade200, backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * context.sf) : null),
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * context.sf)),
onTap: () { onSelected(option); },
);
},
@@ -211,11 +177,11 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
);
},
fieldViewBuilder: (ctx, txtCtrl, node, submit) {
if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) { txtCtrl.text = controller.text; }
if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) txtCtrl.text = controller.text;
txtCtrl.addListener(() { controller.text = txtCtrl.text; });
return TextField(
controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * sf),
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * sf), prefixIcon: Icon(Icons.search, size: 20 * sf), border: const OutlineInputBorder()),
controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * context.sf),
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * context.sf), prefixIcon: Icon(Icons.search, size: 20 * context.sf), border: const OutlineInputBorder()),
);
},
);
@@ -224,7 +190,7 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
}
}
// --- PÁGINA PRINCIPAL DOS JOGOS COM FILTROS ---
// --- PÁGINA PRINCIPAL DOS JOGOS ---
class GamePage extends StatefulWidget {
const GamePage({super.key});
@@ -235,96 +201,54 @@ class GamePage extends StatefulWidget {
class _GamePageState extends State<GamePage> {
final GameController gameController = GameController();
final TeamController teamController = TeamController();
// Variáveis para os filtros
String selectedSeason = 'Todas';
String selectedTeam = 'Todas';
@override
Widget build(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
final double sf = math.min(wScreen, hScreen) / 400;
// Verifica se algum filtro está ativo para mudar a cor do ícone
bool isFilterActive = selectedSeason != 'Todas' || selectedTeam != 'Todas';
return Scaffold(
backgroundColor: const Color(0xFFF5F7FA),
appBar: AppBar(
title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * sf)),
title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)),
backgroundColor: Colors.white,
elevation: 0,
actions: [
// 👇 BOTÃO DE FILTRO NA APP BAR 👇
Padding(
padding: EdgeInsets.only(right: 8.0 * sf),
padding: EdgeInsets.only(right: 8.0 * context.sf),
child: IconButton(
icon: Icon(
isFilterActive ? Icons.filter_list_alt : Icons.filter_list,
color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87,
size: 26 * sf,
),
onPressed: () => _showFilterPopup(context, sf),
icon: Icon(isFilterActive ? Icons.filter_list_alt : Icons.filter_list, color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87, size: 26 * context.sf),
onPressed: () => _showFilterPopup(context),
),
)
],
),
body: StreamBuilder<List<Map<String, dynamic>>>(
stream: teamController.teamsStream,
builder: (context, teamSnapshot) {
final List<Map<String, dynamic>> teamsList = teamSnapshot.data ?? [];
return StreamBuilder<List<Game>>(
stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason),
builder: (context, gameSnapshot) {
if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (gameSnapshot.hasError) {
return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * sf)));
}
if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) return const Center(child: CircularProgressIndicator());
if (gameSnapshot.hasError) return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * context.sf)));
if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 48 * sf, color: Colors.grey.shade300),
SizedBox(height: 10 * sf),
Text("Nenhum jogo encontrado para este filtro.", style: TextStyle(fontSize: 14 * sf, color: Colors.grey.shade600)),
],
)
);
return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.search_off, size: 48 * context.sf, color: Colors.grey.shade300), SizedBox(height: 10 * context.sf), Text("Nenhum jogo encontrado.", style: TextStyle(fontSize: 14 * context.sf, color: Colors.grey.shade600))]));
}
return ListView.builder(
padding: EdgeInsets.all(16 * sf),
padding: EdgeInsets.all(16 * context.sf),
itemCount: gameSnapshot.data!.length,
itemBuilder: (context, index) {
final game = gameSnapshot.data![index];
String? myLogo;
String? oppLogo;
String? myLogo, oppLogo;
for (var team in teamsList) {
if (team['name'] == game.myTeam) { myLogo = team['image_url']; }
if (team['name'] == game.opponentTeam) { oppLogo = team['image_url']; }
if (team['name'] == game.myTeam) myLogo = team['image_url'];
if (team['name'] == game.opponentTeam) oppLogo = team['image_url'];
}
return GameResultCard(
gameId: game.id,
myTeam: game.myTeam,
opponentTeam: game.opponentTeam,
myScore: game.myScore,
opponentScore: game.opponentScore,
status: game.status,
season: game.season,
myTeamLogo: myLogo,
opponentTeamLogo: oppLogo,
sf: sf,
gameId: game.id, myTeam: game.myTeam, opponentTeam: game.opponentTeam, myScore: game.myScore,
opponentScore: game.opponentScore, status: game.status, season: game.season, myTeamLogo: myLogo, opponentTeamLogo: oppLogo,
);
},
);
@@ -334,92 +258,60 @@ class _GamePageState extends State<GamePage> {
),
floatingActionButton: FloatingActionButton(
backgroundColor: const Color(0xFFE74C3C),
child: Icon(Icons.add, color: Colors.white, size: 24 * sf),
onPressed: () => _showCreateDialog(context, sf),
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
onPressed: () => showDialog(context: context, builder: (context) => CreateGameDialogManual(teamController: teamController, gameController: gameController)),
),
);
}
// 👇 O POPUP DE FILTROS 👇
void _showFilterPopup(BuildContext context, double sf) {
// Variáveis temporárias para o Popup (para não atualizar a lista antes de clicar em "Aplicar")
void _showFilterPopup(BuildContext context) {
String tempSeason = selectedSeason;
String tempTeam = selectedTeam;
showDialog(
context: context,
builder: (context) {
// StatefulBuilder permite atualizar a interface APENAS dentro do Popup
return StatefulBuilder(
builder: (context, setPopupState) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * sf)),
IconButton(
icon: const Icon(Icons.close, color: Colors.grey),
onPressed: () => Navigator.pop(context),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
)
Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)),
IconButton(icon: const Icon(Icons.close, color: Colors.grey), onPressed: () => Navigator.pop(context), padding: EdgeInsets.zero, constraints: const BoxConstraints())
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Filtro de Temporada
Text("Temporada", style: TextStyle(fontSize: 12 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * sf),
Text("Temporada", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * context.sf),
Container(
padding: EdgeInsets.symmetric(horizontal: 12 * sf),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * sf)),
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
value: tempSeason,
style: TextStyle(fontSize: 14 * sf, color: Colors.black87, fontWeight: FontWeight.bold),
items: ['Todas', '2024/25', '2025/26'].map((String value) {
return DropdownMenuItem<String>(value: value, child: Text(value));
}).toList(),
onChanged: (newValue) {
setPopupState(() => tempSeason = newValue!);
},
isExpanded: true, value: tempSeason, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold),
items: ['Todas', '2024/25', '2025/26'].map((String value) => DropdownMenuItem<String>(value: value, child: Text(value))).toList(),
onChanged: (newValue) => setPopupState(() => tempSeason = newValue!),
),
),
),
SizedBox(height: 20 * sf),
// 2. Filtro de Equipa
Text("Equipa", style: TextStyle(fontSize: 12 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * sf),
SizedBox(height: 20 * context.sf),
Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * context.sf),
Container(
padding: EdgeInsets.symmetric(horizontal: 12 * sf),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * sf)),
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)),
child: StreamBuilder<List<Map<String, dynamic>>>(
stream: teamController.teamsStream,
builder: (context, snapshot) {
List<String> teamNames = ['Todas'];
if (snapshot.hasData) {
teamNames.addAll(snapshot.data!.map((t) => t['name'].toString()));
}
if (snapshot.hasData) teamNames.addAll(snapshot.data!.map((t) => t['name'].toString()));
if (!teamNames.contains(tempTeam)) tempTeam = 'Todas';
return DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
value: tempTeam,
style: TextStyle(fontSize: 14 * sf, color: Colors.black87, fontWeight: FontWeight.bold),
items: teamNames.map((String value) {
return DropdownMenuItem<String>(value: value, child: Text(value, overflow: TextOverflow.ellipsis));
}).toList(),
onChanged: (newValue) {
setPopupState(() => tempTeam = newValue!);
},
isExpanded: true, value: tempTeam, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold),
items: teamNames.map((String value) => DropdownMenuItem<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: [
TextButton(
onPressed: () {
// Limpar Filtros
setState(() {
selectedSeason = 'Todas';
selectedTeam = 'Todas';
});
Navigator.pop(context);
},
child: Text('LIMPAR', style: TextStyle(fontSize: 12 * sf, color: Colors.grey))
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * sf)),
),
onPressed: () {
// Aplicar Filtros (atualiza a página principal)
setState(() {
selectedSeason = tempSeason;
selectedTeam = tempTeam;
});
Navigator.pop(context);
},
child: Text('APLICAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * sf)),
),
TextButton(onPressed: () { setState(() { selectedSeason = 'Todas'; selectedTeam = 'Todas'; }); Navigator.pop(context); }, child: Text('LIMPAR', style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey))),
ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), onPressed: () { setState(() { selectedSeason = tempSeason; selectedTeam = tempTeam; }); Navigator.pop(context); }, child: Text('APLICAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * context.sf))),
],
);
}
@@ -461,15 +329,4 @@ class _GamePageState extends State<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:supabase_flutter/supabase_flutter.dart';
import 'package:playmaker/pages/status_page.dart';
import 'dart:math' as math;
import '../utils/size_extension.dart';
import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart';
class HomeScreen extends StatefulWidget {
@@ -31,12 +30,10 @@ class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
final double sf = math.min(wScreen, hScreen) / 400;
// Já não precisamos calcular o sf aqui!
final List<Widget> pages = [
_buildHomeContent(sf, wScreen),
_buildHomeContent(context), // Passamos só o context
const GamePage(),
const TeamsPage(),
const StatusPage(),
@@ -45,11 +42,11 @@ class _HomeScreenState extends State<HomeScreen> {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * sf)),
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)),
backgroundColor: HomeConfig.primaryColor,
foregroundColor: Colors.white,
leading: IconButton(
icon: Icon(Icons.person, size: 24 * sf),
icon: Icon(Icons.person, size: 24 * context.sf),
onPressed: () {},
),
),
@@ -65,7 +62,8 @@ class _HomeScreenState extends State<HomeScreen> {
backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
elevation: 1,
height: 70 * math.min(sf, 1.2),
// O math.min não é necessário se já tens o sf. Mas podes usar context.sf
height: 70 * (context.sf < 1.2 ? context.sf : 1.2),
destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'),
NavigationDestination(icon: Icon(Icons.sports_soccer_outlined), selectedIcon: Icon(Icons.sports_soccer), label: 'Jogo'),
@@ -76,16 +74,16 @@ class _HomeScreenState extends State<HomeScreen> {
);
}
void _showTeamSelector(BuildContext context, double sf) {
void _showTeamSelector(BuildContext context) {
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * sf))),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf))),
builder: (context) {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: _teamController.teamsStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * sf, child: const Center(child: Text("Nenhuma equipa criada.")));
if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * context.sf, child: const Center(child: Text("Nenhuma equipa criada.")));
final teams = snapshot.data!;
return ListView.builder(
@@ -114,8 +112,9 @@ class _HomeScreenState extends State<HomeScreen> {
);
}
Widget _buildHomeContent(double sf, double wScreen) {
final double cardHeight = (wScreen / 2) * 1.0;
Widget _buildHomeContent(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double cardHeight = wScreen * 0.5;
return StreamBuilder<List<Map<String, dynamic>>>(
stream: _selectedTeamId != null
@@ -126,44 +125,44 @@ class _HomeScreenState extends State<HomeScreen> {
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 22.0 * sf, vertical: 16.0 * sf),
padding: EdgeInsets.symmetric(horizontal: 22.0 * context.sf, vertical: 16.0 * context.sf),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () => _showTeamSelector(context, sf),
onTap: () => _showTeamSelector(context),
child: Container(
padding: EdgeInsets.all(12 * sf),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * sf), border: Border.all(color: Colors.grey.shade300)),
padding: EdgeInsets.all(12 * context.sf),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.shade300)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(children: [Icon(Icons.shield, color: HomeConfig.primaryColor, size: 24 * sf), SizedBox(width: 10 * sf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold))]),
Row(children: [Icon(Icons.shield, color: HomeConfig.primaryColor, size: 24 * context.sf), SizedBox(width: 10 * context.sf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold))]),
const Icon(Icons.arrow_drop_down),
],
),
),
),
SizedBox(height: 20 * sf),
SizedBox(height: 20 * context.sf),
SizedBox(
height: cardHeight,
child: Row(
children: [
Expanded(child: _buildStatCard(title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)),
SizedBox(width: 12 * sf),
Expanded(child: _buildStatCard(title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))),
Expanded(child: _buildStatCard(context: context, title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)),
SizedBox(width: 12 * context.sf),
Expanded(child: _buildStatCard(context: context, title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))),
],
),
),
SizedBox(height: 12 * sf),
SizedBox(height: 12 * context.sf),
SizedBox(
height: cardHeight,
child: Row(
children: [
Expanded(child: _buildStatCard(title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))),
SizedBox(width: 12 * sf),
Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))),
SizedBox(width: 12 * context.sf),
Expanded(
child: PieChartCard(
victories: _teamWins,
@@ -172,58 +171,42 @@ class _HomeScreenState extends State<HomeScreen> {
title: 'DESEMPENHO',
subtitle: 'Temporada',
backgroundColor: const Color(0xFFC62828),
sf: sf
sf: context.sf // Aqui o PieChartCard ainda usa sf, então passamos
),
),
],
),
),
SizedBox(height: 40 * sf),
SizedBox(height: 40 * context.sf),
Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * sf, fontWeight: FontWeight.bold, color: Colors.grey[800])),
SizedBox(height: 16 * sf),
Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: Colors.grey[800])),
SizedBox(height: 16 * context.sf),
// 👇 HISTÓRICO LIGADO À BASE DE DADOS (COM AS COLUNAS CORRIGIDAS) 👇
// 👇 LIGAÇÃO CORRIGIDA: Agora usa a coluna 'nome' como pediste 👇
// 👇 HISTÓRICO DINÂMICO (Qualquer equipa) 👇
_selectedTeamName == "Selecionar Equipa"
? Container(
padding: EdgeInsets.all(20 * sf),
padding: EdgeInsets.all(20 * context.sf),
alignment: Alignment.center,
child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf)),
child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)),
)
: StreamBuilder<List<Map<String, dynamic>>>(
// Pede os jogos ordenados pela data (sem filtros rígidos aqui)
stream: _supabase.from('games').stream(primaryKey: ['id'])
.order('game_date', ascending: false),
builder: (context, gameSnapshot) {
if (gameSnapshot.hasError) {
return Text("Erro ao carregar jogos: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
}
if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
if (gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
if (gameSnapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
// 👇 O CÉREBRO DA APP: Filtro inteligente no Flutter 👇
final todosOsJogos = gameSnapshot.data ?? [];
final gamesList = todosOsJogos.where((game) {
String myT = game['my_team']?.toString() ?? '';
String oppT = game['opponent_team']?.toString() ?? '';
String status = game['status']?.toString() ?? '';
// O jogo tem de envolver a equipa selecionada E estar Terminado
bool isPlaying = (myT == _selectedTeamName || oppT == _selectedTeamName);
bool isFinished = status == 'Terminado';
return isPlaying && isFinished;
}).take(3).toList(); // Pega apenas nos 3 mais recentes
return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado';
}).take(3).toList();
if (gamesList.isEmpty) {
return Container(
padding: EdgeInsets.all(20 * sf),
padding: EdgeInsets.all(20 * context.sf),
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)),
alignment: Alignment.center,
child: Text("Ainda não há jogos terminados para $_selectedTeamName.", style: TextStyle(color: Colors.grey)),
@@ -232,47 +215,33 @@ class _HomeScreenState extends State<HomeScreen> {
return Column(
children: gamesList.map((game) {
// Lê os dados brutos da base de dados
String dbMyTeam = game['my_team']?.toString() ?? '';
String dbOppTeam = game['opponent_team']?.toString() ?? '';
int dbMyScore = int.tryParse(game['my_score'].toString()) ?? 0;
int dbOppScore = int.tryParse(game['opponent_score'].toString()) ?? 0;
String opponent;
int myScore;
int oppScore;
String opponent; int myScore; int oppScore;
// 🔄 MAGIA DA INVERSÃO DE RESULTADOS 🔄
// Garante que os pontos da equipa selecionada aparecem sempre do lado esquerdo
if (dbMyTeam == _selectedTeamName) {
// A equipa que escolhemos está guardada no 'my_team'
opponent = dbOppTeam;
myScore = dbMyScore;
oppScore = dbOppScore;
opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore;
} else {
// A equipa que escolhemos está guardada no 'opponent_team'
opponent = dbMyTeam;
myScore = dbOppScore;
oppScore = dbMyScore;
opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore;
}
// Limpa a data (Remove as horas e deixa só YYYY-MM-DD)
String rawDate = game['game_date']?.toString() ?? '---';
String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
// Calcula Vitória, Empate ou Derrota para a equipa selecionada
String result = 'E';
if (myScore > oppScore) result = 'V';
if (myScore < oppScore) result = 'D';
return _buildGameHistoryCard(
context: context, // Usamos o context para o sf
opponent: opponent,
result: result,
myScore: myScore,
oppScore: oppScore,
date: date,
sf: sf,
topPts: game['top_pts_name'] ?? '---',
topAst: game['top_ast_name'] ?? '---',
topRbs: game['top_rbs_name'] ?? '---',
@@ -284,7 +253,7 @@ class _HomeScreenState extends State<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)};
}
Widget _buildStatCard({required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) {
Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) {
return Card(
elevation: 4, margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none),
@@ -329,8 +298,7 @@ class _HomeScreenState extends State<HomeScreen> {
SizedBox(
width: double.infinity,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
fit: BoxFit.scaleDown, alignment: Alignment.centerLeft,
child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)),
),
),
@@ -340,8 +308,7 @@ class _HomeScreenState extends State<HomeScreen> {
Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))),
const Spacer(),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: ch * 0.035),
width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035),
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)),
child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold)))
),
@@ -355,7 +322,7 @@ class _HomeScreenState extends State<HomeScreen> {
}
Widget _buildGameHistoryCard({
required String opponent, required String result, required int myScore, required int oppScore, required String date, required double sf,
required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date,
required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp
}) {
bool isWin = result == 'V';
@@ -363,44 +330,42 @@ class _HomeScreenState extends State<HomeScreen> {
Color statusColor = isWin ? Colors.green : (isDraw ? Colors.yellow.shade700 : Colors.red);
return Container(
margin: EdgeInsets.only(bottom: 14 * sf),
margin: EdgeInsets.only(bottom: 14 * context.sf),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))],
color: Colors.white, borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))],
),
child: Column(
children: [
Padding(
padding: EdgeInsets.all(14 * sf),
padding: EdgeInsets.all(14 * context.sf),
child: Row(
children: [
Container(
width: 36 * sf, height: 36 * sf,
width: 36 * context.sf, height: 36 * context.sf,
decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle),
child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * sf))),
child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf))),
),
SizedBox(width: 14 * sf),
SizedBox(width: 14 * context.sf),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(date, style: TextStyle(fontSize: 11 * sf, color: Colors.grey, fontWeight: FontWeight.w600)),
SizedBox(height: 6 * sf),
Text(date, style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.w600)),
SizedBox(height: 6 * context.sf),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)),
Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8 * sf),
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 4 * sf),
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8)),
child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)),
child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)),
),
),
Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)),
Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)),
],
),
],
@@ -409,35 +374,29 @@ class _HomeScreenState extends State<HomeScreen> {
],
),
),
Divider(height: 1, color: Colors.grey.shade100, thickness: 1.5),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 12 * sf),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16)),
),
width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf),
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))),
child: Column(
children: [
Row(
children: [
Expanded(child: _buildGridStatRow(Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, sf, isMvp: true)),
Expanded(child: _buildGridStatRow(Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef, sf)),
Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)),
Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef)),
],
),
SizedBox(height: 8 * sf),
SizedBox(height: 8 * context.sf),
Row(
children: [
Expanded(child: _buildGridStatRow(Icons.bolt, Colors.blue.shade700, "Pontos", topPts, sf)),
Expanded(child: _buildGridStatRow(Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs, sf)),
Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)),
Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs)),
],
),
SizedBox(height: 8 * sf),
SizedBox(height: 8 * context.sf),
Row(
children: [
Expanded(child: _buildGridStatRow(Icons.star, Colors.green.shade700, "Assists", topAst, sf)),
Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)),
const Expanded(child: SizedBox()),
],
),
@@ -449,22 +408,21 @@ class _HomeScreenState extends State<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(
children: [
Icon(icon, size: 14 * sf, color: color),
SizedBox(width: 4 * sf),
Text('$label: ', style: TextStyle(fontSize: 11 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
Icon(icon, size: 14 * context.sf, color: color),
SizedBox(width: 4 * context.sf),
Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 11 * sf,
fontSize: 11 * context.sf,
color: isMvp ? Colors.amber.shade900 : Colors.black87,
fontWeight: FontWeight.bold
),
maxLines: 1,
overflow: TextOverflow.ellipsis
maxLines: 1, overflow: TextOverflow.ellipsis
)
),
],

View File

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

View File

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

View File

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

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/pages/RegisterPage.dart';
import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
class BasketTrackHeader extends StatelessWidget {
const BasketTrackHeader({super.key});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
// TAMANHOS AUMENTADOS para tablets
final logoSize = screenWidth > 600 ? 400.0 : 300.0; // ↑ Aumentado
final titleFontSize = screenWidth > 600 ? 48.0 : 36.0; // ↑ Aumentado
final subtitleFontSize = screenWidth > 600 ? 22.0 : 18.0; // ↑ Aumentado
return Column(
children: [
Container(
width: logoSize,
height: logoSize,
child: Image.asset(
'assets/playmaker-logo.png',
fit: BoxFit.contain,
),
),
SizedBox(height: screenWidth > 600 ? 1.0 : 1.0),
Text(
'BasketTrack',
style: TextStyle(
fontSize: titleFontSize,
fontWeight: FontWeight.bold,
color: Colors.grey[900],
),
),
SizedBox(height: screenWidth > 600 ? 1.0 : 1.0),
Text(
'Gere as tuas equipas e estatísticas',
style: TextStyle(
fontSize: subtitleFontSize,
color: Colors.grey[600],
fontWeight: FontWeight.w500, // ↑ Adicionado peso da fonte
),
textAlign: TextAlign.center,
),
],
);
}
}
class LoginFormFields extends StatelessWidget {
final LoginController controller;
const LoginFormFields({super.key, required this.controller});
class BasketTrackHeader extends StatelessWidget {
const BasketTrackHeader({super.key});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final verticalPadding = screenWidth > 600 ? 22.0 : 16.0;
return Column(
children: [
SizedBox(
width: 300 * context.sf, // Ajusta o tamanho da imagem suavemente
height: 300 * context.sf,
child: Image.asset(
'assets/playmaker-logo.png',
fit: BoxFit.contain,
),
),
Text(
'BasketTrack',
style: TextStyle(
fontSize: 36 * context.sf,
fontWeight: FontWeight.bold,
color: Colors.grey[900],
),
),
SizedBox(height: 6 * context.sf),
Text(
'Gere as tuas equipas e estatísticas',
style: TextStyle(
fontSize: 16 * context.sf,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
);
}
}
class LoginFormFields extends StatelessWidget {
final LoginController controller;
const LoginFormFields({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: controller.emailController,
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'E-mail',
prefixIcon: const Icon(Icons.email_outlined),
// O erro agora vem diretamente do controller
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.email_outlined, size: 22 * context.sf),
errorText: controller.emailError,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 20),
SizedBox(height: 20 * context.sf),
TextField(
controller: controller.passwordController,
obscureText: controller.obscurePassword,
style: TextStyle(fontSize: 15 * context.sf),
decoration: InputDecoration(
labelText: 'Palavra-passe',
prefixIcon: const Icon(Icons.lock_outlined),
labelStyle: TextStyle(fontSize: 15 * context.sf),
prefixIcon: Icon(Icons.lock_outlined, size: 22 * context.sf),
errorText: controller.passwordError,
suffixIcon: IconButton(
icon: Icon(controller.obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined),
icon: Icon(
controller.obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
size: 22 * context.sf
),
onPressed: controller.togglePasswordVisibility,
),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: 16),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12 * context.sf)),
contentPadding: EdgeInsets.symmetric(vertical: 18 * context.sf, horizontal: 16 * context.sf),
),
),
],
);
}
}
class LoginButton extends StatelessWidget {
final LoginController controller;
final VoidCallback onLoginSuccess;
const LoginButton({
super.key,
required this.controller,
required this.onLoginSuccess,
});
class LoginButton extends StatelessWidget {
final LoginController controller;
final VoidCallback onLoginSuccess;
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
// BOTÕES MAIORES
final buttonHeight = screenWidth > 600 ? 70.0 : 58.0; // ↑ Aumentado
final fontSize = screenWidth > 600 ? 22.0 : 18.0; // ↑ Aumentado
const LoginButton({super.key, required this.controller, required this.onLoginSuccess});
return SizedBox(
width: double.infinity,
height: buttonHeight,
child: ElevatedButton(
onPressed: controller.isLoading ? null : () async {
final success = await controller.login();
if (success) {
onLoginSuccess();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14), // ↑ Bordas mais arredondadas
),
elevation: 3, // ↑ Sombra mais pronunciada
),
child: controller.isLoading
? SizedBox(
width: 28, // ↑ Aumentado
height: 28, // ↑ Aumentado
child: CircularProgressIndicator(
strokeWidth: 3, // ↑ Aumentado
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'Entrar',
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w700, //
),
),
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 58 * context.sf,
child: ElevatedButton(
onPressed: controller.isLoading ? null : () async {
final success = await controller.login();
if (success) onLoginSuccess();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
elevation: 3,
),
);
}
child: controller.isLoading
? SizedBox(
width: 28 * context.sf, height: 28 * context.sf,
child: const CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(Colors.white)),
)
: Text('Entrar', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
),
);
}
}
class CreateAccountButton extends StatelessWidget {
const CreateAccountButton({super.key});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final buttonHeight = screenWidth > 600 ? 70.0 : 58.0;
final fontSize = screenWidth > 600 ? 22.0 : 18.0;
return SizedBox(
width: double.infinity,
height: buttonHeight,
height: 58 * context.sf,
child: OutlinedButton(
onPressed: () {
// Navega para a página de registo que criaste
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const RegisterPage()),
);
Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterPage()));
},
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFE74C3C),
side: const BorderSide(color: Color(0xFFE74C3C), width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: Text(
'Criar Conta',
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w700,
),
side: BorderSide(color: const Color(0xFFE74C3C), width: 2 * context.sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * context.sf)),
),
child: Text('Criar Conta', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
),
);
}
}
}

View File

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

View File

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

View File

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