git lixo 2

This commit is contained in:
2026-03-16 23:25:48 +00:00
parent a4ef651d64
commit ec5bdc4867
15 changed files with 974 additions and 1072 deletions

View File

@@ -5,7 +5,7 @@ class GameController {
final _supabase = Supabase.instance.client; final _supabase = Supabase.instance.client;
// 1. LER JOGOS (Stream em Tempo Real) // 1. LER JOGOS (Stream em Tempo Real)
Stream<List<Game>> get gamesStream { Stream<List<Game>> get gamesStream {
return _supabase return _supabase
.from('games') // 1. Fica à escuta da tabela original (Garante o Tempo Real!) .from('games') // 1. Fica à escuta da tabela original (Garante o Tempo Real!)
.stream(primaryKey: ['id']) .stream(primaryKey: ['id'])
@@ -21,6 +21,40 @@ Stream<List<Game>> get gamesStream {
return viewData.map((json) => Game.fromMap(json)).toList(); return viewData.map((json) => Game.fromMap(json)).toList();
}); });
} }
// =========================================================================
// 👇 NOVO: LER JOGOS COM FILTROS DE EQUIPA E TEMPORADA (MANTÉM OS LOGOS)
// =========================================================================
// =========================================================================
// 👇 LER JOGOS COM FILTROS DE EQUIPA E TEMPORADA (SEM ERROS DE QUERY)
// =========================================================================
Stream<List<Game>> getFilteredGames({required String teamFilter, required String seasonFilter}) {
return _supabase
.from('games')
.stream(primaryKey: ['id'])
.asyncMap((event) async {
// 1. Começamos a query APENAS com o select (Sem o order ainda!)
var query = _supabase.from('games_with_logos').select();
// 2. Se a temporada não for "Todas", aplicamos o filtro AQUI
if (seasonFilter != 'Todas') {
query = query.eq('season', seasonFilter);
}
// 3. Executamos a query e aplicamos o ORDER BY no final
final viewData = await query.order('game_date', ascending: false);
List<Game> games = viewData.map((json) => Game.fromMap(json)).toList();
// 4. Filtramos a equipa em memória
if (teamFilter != 'Todas') {
games = games.where((g) => g.myTeam == teamFilter || g.opponentTeam == teamFilter).toList();
}
return games;
});
}
// 2. CRIAR JOGO // 2. CRIAR JOGO
// Retorna o ID do jogo criado para podermos navegar para o placar // Retorna o ID do jogo criado para podermos navegar para o placar
Future<String?> createGame(String myTeam, String opponent, String season) async { Future<String?> createGame(String myTeam, String opponent, String season) async {

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class HomeController extends ChangeNotifier { class HomeController extends ChangeNotifier {
// Se precisar de estado para a home screen
int _selectedCardIndex = 0; int _selectedCardIndex = 0;
int get selectedCardIndex => _selectedCardIndex; int get selectedCardIndex => _selectedCardIndex;
@@ -11,10 +10,8 @@ class HomeController extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// Métodos adicionais para lógica da home
void navigateToDetails(String playerName) { void navigateToDetails(String playerName) {
print('Navegando para detalhes de $playerName'); print('Navegando para detalhes de $playerName');
// Implementar navegação
} }
void refreshData() { void refreshData() {

View File

@@ -5,7 +5,6 @@ class TeamController {
final _supabase = Supabase.instance.client; final _supabase = Supabase.instance.client;
// 1. STREAM (Realtime) // 1. STREAM (Realtime)
// Adicionei o .map() no final para garantir que o Dart entende que é uma List<Map>
Stream<List<Map<String, dynamic>>> get teamsStream { Stream<List<Map<String, dynamic>>> get teamsStream {
return _supabase return _supabase
.from('teams') .from('teams')
@@ -15,7 +14,6 @@ class TeamController {
} }
// 2. CRIAR // 2. CRIAR
// Alterei imageUrl para String? (pode ser nulo) para evitar erros se não houver imagem
Future<void> createTeam(String name, String season, String? imageUrl) async { Future<void> createTeam(String name, String season, String? imageUrl) async {
try { try {
await _supabase.from('teams').insert({ await _supabase.from('teams').insert({
@@ -51,13 +49,12 @@ class TeamController {
} }
} }
// 5. CONTAR JOGADORES // 5. CONTAR JOGADORES (LEITURA ÚNICA)
// CORRIGIDO: A sintaxe antiga dava erro. O método .count() é o correto agora.
Future<int> getPlayerCount(String teamId) async { Future<int> getPlayerCount(String teamId) async {
try { try {
final count = await _supabase final count = await _supabase
.from('members') .from('members')
.count() // Retorna diretamente o número inteiro .count()
.eq('team_id', teamId); .eq('team_id', teamId);
return count; return count;
} catch (e) { } catch (e) {
@@ -66,6 +63,17 @@ class TeamController {
} }
} }
// 👇 6. A FUNÇÃO QUE RESOLVE O ERRO (EM TEMPO REAL) 👇
Stream<int> getPlayerCountStream(String teamId) {
return _supabase
.from('members')
.stream(primaryKey: ['id'])
.eq('team_id', teamId)
.map((membros) => membros
.where((membro) => membro['type'] == 'Jogador')
.length);
}
// Mantemos o dispose vazio para não quebrar a chamada na TeamsPage // Mantemos o dispose vazio para não quebrar a chamada na TeamsPage
void dispose() {} void dispose() {}
} }

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../dados_grafico.dart'; // Ajusta o caminho se der erro de import import '../dados_grafico.dart';
class PieChartController extends ChangeNotifier { class PieChartController extends ChangeNotifier {
PieChartData _chartData = const PieChartData(victories: 0, defeats: 0, draws: 0); PieChartData _chartData = const PieChartData(victories: 0, defeats: 0, draws: 0);
@@ -10,7 +10,7 @@ class PieChartController extends ChangeNotifier {
_chartData = PieChartData( _chartData = PieChartData(
victories: victories ?? _chartData.victories, victories: victories ?? _chartData.victories,
defeats: defeats ?? _chartData.defeats, defeats: defeats ?? _chartData.defeats,
draws: draws ?? _chartData.draws, // 👇 AGORA ELE ACEITA OS EMPATES draws: draws ?? _chartData.draws,
); );
notifyListeners(); notifyListeners();
} }

View File

@@ -22,5 +22,6 @@ class PieChartData {
'total': total, 'total': total,
'victoryPercentage': victoryPercentage, 'victoryPercentage': victoryPercentage,
'defeatPercentage': defeatPercentage, 'defeatPercentage': defeatPercentage,
'drawPercentage': drawPercentage,
}; };
} }

View File

@@ -1,21 +1,28 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart'; import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart';
import 'dados_grafico.dart'; import 'dados_grafico.dart';
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA ADICIONADO PARA USARMOS O primaryRed
import 'dart:math' as math;
class PieChartCard extends StatefulWidget { class PieChartCard extends StatefulWidget {
final PieChartController? controller; final int victories;
final int defeats;
final int draws;
final String title; final String title;
final String subtitle; final String subtitle;
final Color? backgroundColor; final Color? backgroundColor;
final VoidCallback? onTap; final VoidCallback? onTap;
final double sf;
const PieChartCard({ const PieChartCard({
super.key, super.key,
this.controller, this.victories = 0,
this.defeats = 0,
this.draws = 0,
this.title = 'DESEMPENHO', this.title = 'DESEMPENHO',
this.subtitle = 'Temporada', this.subtitle = 'Temporada',
this.onTap, this.onTap,
required this.backgroundColor, this.backgroundColor,
this.sf = 1.0, this.sf = 1.0,
}); });
@@ -24,30 +31,26 @@ class PieChartCard extends StatefulWidget {
} }
class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderStateMixin { class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderStateMixin {
late PieChartController _controller;
late AnimationController _animationController; late AnimationController _animationController;
late Animation<double> _animation; late Animation<double> _animation;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = widget.controller ?? PieChartController(); _animationController = AnimationController(duration: const Duration(milliseconds: 600), vsync: this);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeOutBack));
_animationController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutBack,
),
);
_animationController.forward(); _animationController.forward();
} }
@override
void didUpdateWidget(PieChartCard oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.victories != widget.victories || oldWidget.defeats != widget.defeats || oldWidget.draws != widget.draws) {
_animationController.reset();
_animationController.forward();
}
}
@override @override
void dispose() { void dispose() {
_animationController.dispose(); _animationController.dispose();
@@ -58,30 +61,31 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
Widget build(BuildContext context) { Widget build(BuildContext context) {
final data = PieChartData(victories: widget.victories, defeats: widget.defeats, draws: widget.draws); final data = PieChartData(victories: widget.victories, defeats: widget.defeats, draws: widget.draws);
return AnimatedBuilder( // 👇 BLINDAGEM DO FUNDO E DO TEXTO PARA MODO CLARO/ESCURO
final Color cardColor = widget.backgroundColor ?? Theme.of(context).cardTheme.color ?? (Theme.of(context).brightness == Brightness.dark ? const Color(0xFF1E1E1E) : Colors.white);
final Color textColor = Theme.of(context).colorScheme.onSurface;
return AnimatedBuilder(
animation: _animation, animation: _animation,
builder: (context, child) { builder: (context, child) {
return Transform.scale( return Transform.scale(
// O scale pode passar de 1.0 (efeito back), mas a opacidade NÃO
scale: 0.95 + (_animation.value * 0.05), scale: 0.95 + (_animation.value * 0.05),
child: Opacity( child: Opacity(opacity: _animation.value.clamp(0.0, 1.0), child: child),
// 👇 AQUI ESTÁ A FIX: Garante que fica entre 0 e 1
opacity: _animation.value.clamp(0.0, 1.0),
child: child,
),
); );
}, },
child: Card( child: Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
elevation: 4, elevation: 0, // Ajustado para não ter sombra dupla, já que o tema pode ter
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: Colors.grey.withOpacity(0.15)), // Borda suave igual ao resto da app
),
child: InkWell( child: InkWell(
onTap: widget.onTap, onTap: widget.onTap,
borderRadius: BorderRadius.circular(14),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14), color: cardColor, // 👇 APLICA A COR BLINDADA
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [widget.backgroundColor.withOpacity(0.9), widget.backgroundColor.withOpacity(0.7)]),
), ),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
@@ -89,29 +93,43 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
final double cw = constraints.maxWidth; final double cw = constraints.maxWidth;
return Padding( return Padding(
padding: EdgeInsets.all(cw * 0.06), padding: EdgeInsets.symmetric(horizontal: cw * 0.05, vertical: ch * 0.03),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 👇 TÍTULOS UM POUCO MAIS PRESENTES // --- CABEÇALHO --- (👇 MANTIDO ALINHADO À ESQUERDA)
FittedBox( FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Text(widget.title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), letterSpacing: 1.0)), child: Text(widget.title.toUpperCase(),
style: TextStyle(
fontSize: ch * 0.045,
fontWeight: FontWeight.bold,
color: AppTheme.primaryRed, // 👇 USANDO O TEU primaryRed
letterSpacing: 1.2
)
),
), ),
FittedBox( Text(widget.subtitle,
fit: BoxFit.scaleDown, style: TextStyle(
child: Text(widget.subtitle, style: TextStyle(fontSize: ch * 0.07, fontWeight: FontWeight.bold, color: Colors.white)), fontSize: ch * 0.055,
fontWeight: FontWeight.bold,
color: AppTheme.backgroundLight, // 👇 USANDO O TEU backgroundLight
)
), ),
SizedBox(height: ch * 0.03), const Expanded(flex: 1, child: SizedBox()),
// MEIO (GRÁFICO + ESTATÍSTICAS) // --- MIOLO (GRÁFICO MAIOR À ESQUERDA + STATS) ---
Expanded( Expanded(
flex: 9,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.end, // Changed from spaceBetween to end to push stats more to the right
children: [ children: [
Expanded( // 1. Lado Esquerdo: Donut Chart
flex: 1, // 👇 MUDANÇA AQUI: Gráfico ainda maior! cw * 0.52
SizedBox(
width: cw * 0.52,
height: cw * 0.52,
child: PieChartWidget( child: PieChartWidget(
victoryPercentage: data.victoryPercentage, victoryPercentage: data.victoryPercentage,
defeatPercentage: data.defeatPercentage, defeatPercentage: data.defeatPercentage,
@@ -119,131 +137,103 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
sf: widget.sf, sf: widget.sf,
), ),
), ),
SizedBox(width: cw * 0.05),
SizedBox(width: cw * 0.005), // Reduzi o espaço no meio para dar lugar ao gráfico
// 2. Lado Direito: Números Dinâmicos
Expanded( Expanded(
flex: 1, child: FittedBox(
child: Column( alignment: Alignment.centerRight, // Encosta os números à direita
mainAxisAlignment: MainAxisAlignment.start, fit: BoxFit.scaleDown,
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
_buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, ch), crossAxisAlignment: CrossAxisAlignment.end, // Alinha os números à direita para ficar arrumado
_buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.yellow, ch), children: [
_buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, ch), _buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, textColor, ch, cw),
_buildDynDivider(ch), _buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.amber, textColor, ch, cw),
_buildDynStatRow("TOT", data.total.toString(), "100", Colors.white, ch), _buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, textColor, ch, cw),
], _buildDynDivider(cw, textColor),
_buildDynStatRow("TOT", data.total.toString(), "100", textColor, textColor, ch, cw),
],
),
), ),
), ),
], ],
), ),
), ),
// 👇 RODAPÉ AJUSTADO const Expanded(flex: 1, child: SizedBox()),
SizedBox(height: ch * 0.03),
// --- RODAPÉ: BOTÃO WIN RATE GIGANTE --- (👇 MUDANÇA AQUI: Alinhado à esquerda)
Container( Container(
width: double.infinity, width: double.infinity,
padding: EdgeInsets.symmetric(vertical: ch * 0.035), padding: EdgeInsets.symmetric(vertical: ch * 0.025),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white24, // Igual ao fundo do botão detalhes color: textColor.withOpacity(0.05), // 👇 Fundo adaptável
borderRadius: BorderRadius.circular(ch * 0.03), // Borda arredondada borderRadius: BorderRadius.circular(12),
), ),
child: Center( child: FittedBox(
child: FittedBox( fit: BoxFit.scaleDown,
fit: BoxFit.scaleDown, child: Row(
child: Row( mainAxisAlignment: MainAxisAlignment.start, // 👇 MUDANÇA AQUI: Letras mais para a esquerda!
mainAxisAlignment: MainAxisAlignment.center, children: [
children: [ Icon(Icons.stars, color: Colors.green, size: ch * 0.075),
Icon( const SizedBox(width: 10),
data.victoryPercentage >= 0.5 ? Icons.trending_up : Icons.trending_down, Text('WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
color: Colors.green, style: TextStyle(
size: ch * 0.09 color: AppTheme.backgroundLight,
), fontWeight: FontWeight.w900,
SizedBox(width: cw * 0.02), letterSpacing: 1.0,
Text( fontSize: ch * 0.06
'WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: ch * 0.05,
fontWeight: FontWeight.bold,
color: Colors.white
)
), ),
), ),
], ],
), ),
], ),
), ),
), ],
),
SizedBox(height: 10), // Espaço controlado );
}
// Win rate - Sempre visível e não sobreposto
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
data.victoryPercentage > 0.5
? Icons.trending_up
: Icons.trending_down,
color: data.victoryPercentage > 0.5
? Colors.green
: Colors.red,
size: 18, // Pequeno
),
SizedBox(width: 8),
Text(
'Win Rate: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 14, // Pequeno
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
],
),
),
), ),
), ),
), ),
), ),
); );
} }
// 👇 PERCENTAGENS SUBIDAS LIGEIRAMENTE (0.10 e 0.045)
Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch) { // 👇 Ajustei a linha de stats para alinhar melhor agora que os números estão encostados à direita
Widget _buildDynStatRow(String label, String number, String percent, Color statColor, Color textColor, double ch, double cw) {
return Padding( return Padding(
padding: EdgeInsets.only(bottom: ch * 0.01), padding: EdgeInsets.symmetric(vertical: ch * 0.005),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Número subiu para 0.10 SizedBox(
Expanded(flex: 2, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(number, style: TextStyle(fontSize: ch * 0.10, fontWeight: FontWeight.bold, color: color, height: 1.0)))), width: cw * 0.12,
SizedBox(width: ch * 0.02), child: Column(
Expanded( crossAxisAlignment: CrossAxisAlignment.end,
flex: 3, mainAxisSize: MainAxisSize.min,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ children: [
Row(children: [ Text(label, style: TextStyle(fontSize: ch * 0.045, color: textColor.withOpacity(0.6), fontWeight: FontWeight.bold)), // 👇 TEXTO ADAPTÁVEL (increased from 0.035)
Container(width: ch * 0.018, height: ch * 0.018, margin: EdgeInsets.only(right: ch * 0.015), decoration: BoxDecoration(color: color, shape: BoxShape.circle)), Text('$percent%', style: TextStyle(fontSize: ch * 0.05, color: statColor, fontWeight: FontWeight.bold)), // (increased from 0.04)
// Label subiu para 0.045 ],
Expanded(child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(label, style: TextStyle(fontSize: ch * 0.033, color: Colors.white.withOpacity(0.8), fontWeight: FontWeight.w600)))) ),
]),
// Percentagem subiu para 0.05
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('$percent%', style: TextStyle(fontSize: ch * 0.04, color: color, fontWeight: FontWeight.bold))),
]),
), ),
SizedBox(width: cw * 0.03),
Text(number, style: TextStyle(fontSize: ch * 0.15, fontWeight: FontWeight.w900, color: statColor, height: 1)), // (increased from 0.125)
], ],
), ),
); );
} }
Widget _buildDynDivider(double ch) { Widget _buildDynDivider(double cw, Color textColor) {
return Container(height: 0.5, color: Colors.white.withOpacity(0.1), margin: EdgeInsets.symmetric(vertical: ch * 0.01)); return Container(
width: cw * 0.35,
height: 1.5,
color: textColor.withOpacity(0.2), // 👇 LINHA ADAPTÁVEL
margin: const EdgeInsets.symmetric(vertical: 4)
);
} }
} }

View File

@@ -5,26 +5,23 @@ class PieChartWidget extends StatelessWidget {
final double victoryPercentage; final double victoryPercentage;
final double defeatPercentage; final double defeatPercentage;
final double drawPercentage; final double drawPercentage;
final double size; final double sf;
const PieChartWidget({ const PieChartWidget({
super.key, super.key,
required this.victoryPercentage, required this.victoryPercentage,
required this.defeatPercentage, required this.defeatPercentage,
this.drawPercentage = 0, this.drawPercentage = 0,
this.size = 140, // Aumentado para 400x300 required this.sf,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
// 👇 MAGIA ANTI-DESAPARECIMENTO 👇
// Vê o espaço real. Se por algum motivo for infinito, assume 100 para não sumir.
final double w = constraints.maxWidth.isInfinite ? 100.0 : constraints.maxWidth; final double w = constraints.maxWidth.isInfinite ? 100.0 : constraints.maxWidth;
final double h = constraints.maxHeight.isInfinite ? 100.0 : constraints.maxHeight; final double h = constraints.maxHeight.isInfinite ? 100.0 : constraints.maxHeight;
// Pega no menor valor para garantir que o círculo não é cortado
final double size = math.min(w, h); final double size = math.min(w, h);
return Center( return Center(
@@ -32,7 +29,7 @@ class PieChartWidget extends StatelessWidget {
width: size, width: size,
height: size, height: size,
child: CustomPaint( child: CustomPaint(
painter: _PieChartPainter( painter: _DonutChartPainter(
victoryPercentage: victoryPercentage, victoryPercentage: victoryPercentage,
defeatPercentage: defeatPercentage, defeatPercentage: defeatPercentage,
drawPercentage: drawPercentage, drawPercentage: drawPercentage,
@@ -48,24 +45,27 @@ class PieChartWidget extends StatelessWidget {
} }
Widget _buildCenterLabels(double size) { Widget _buildCenterLabels(double size) {
final bool hasGames = victoryPercentage > 0 || defeatPercentage > 0 || drawPercentage > 0;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
'${(victoryPercentage * 100).toStringAsFixed(1)}%', // 👇 Casa decimal aplicada aqui!
hasGames ? '${(victoryPercentage * 100).toStringAsFixed(1)}%' : '---',
style: TextStyle( style: TextStyle(
fontSize: size * 0.18, // O texto cresce ou encolhe com o círculo fontSize: size * (hasGames ? 0.20 : 0.15),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: hasGames ? Colors.white : Colors.white54,
), ),
), ),
SizedBox(height: size * 0.02), SizedBox(height: size * 0.02),
Text( Text(
'Vitórias', hasGames ? 'Vitórias' : 'Sem Jogos',
style: TextStyle( style: TextStyle(
fontSize: size * 0.10, fontSize: size * 0.08,
color: Colors.white.withOpacity(0.8), color: hasGames ? Colors.white70 : Colors.white38,
), ),
), ),
], ],
@@ -87,59 +87,40 @@ class _DonutChartPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2); final center = Offset(size.width / 2, size.height / 2);
// Margem de 5% para a linha de fora não ser cortada final radius = (size.width / 2) - (size.width * 0.1);
final radius = (size.width / 2) - (size.width * 0.05); final strokeWidth = size.width * 0.2;
if (victoryPercentage == 0 && defeatPercentage == 0 && drawPercentage == 0) {
final bgPaint = Paint()
..color = Colors.white.withOpacity(0.05)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth;
canvas.drawCircle(center, radius, bgPaint);
return;
}
const victoryColor = Colors.green; const victoryColor = Colors.green;
const defeatColor = Colors.red; const defeatColor = Colors.red;
const drawColor = Colors.yellow; const drawColor = Colors.amber;
const borderColor = Colors.white30;
double startAngle = -math.pi / 2; double startAngle = -math.pi / 2;
if (victoryPercentage > 0) { void drawDonutSector(double percentage, Color color) {
final sweepAngle = 2 * math.pi * victoryPercentage; if (percentage <= 0) return;
_drawSector(canvas, center, radius, startAngle, sweepAngle, victoryColor, size.width); final sweepAngle = 2 * math.pi * percentage;
startAngle += sweepAngle; final paint = Paint()
} ..color = color
if (drawPercentage > 0) {
final sweepAngle = 2 * math.pi * drawPercentage;
_drawSector(canvas, center, radius, startAngle, sweepAngle, drawColor, size.width);
startAngle += sweepAngle;
}
if (defeatPercentage > 0) {
final sweepAngle = 2 * math.pi * defeatPercentage;
_drawSector(canvas, center, radius, startAngle, sweepAngle, defeatColor, size.width);
}
final borderPaint = Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = size.width * 0.02;
canvas.drawCircle(center, radius, borderPaint);
}
void _drawSector(Canvas canvas, Offset center, double radius, double startAngle, double sweepAngle, Color color, double totalWidth) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, true, paint);
if (sweepAngle < 2 * math.pi) {
final linePaint = Paint()
..color = Colors.white.withOpacity(0.5)
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = totalWidth * 0.015; ..strokeWidth = strokeWidth
..strokeCap = StrokeCap.butt;
final lineX = center.dx + radius * math.cos(startAngle); canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, paint);
final lineY = center.dy + radius * math.sin(startAngle); startAngle += sweepAngle;
canvas.drawLine(center, Offset(lineX, lineY), linePaint);
} }
drawDonutSector(victoryPercentage, victoryColor);
drawDonutSector(drawPercentage, drawColor);
drawDonutSector(defeatPercentage, defeatColor);
} }
@override @override

View File

@@ -1,77 +1,91 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/pages/PlacarPage.dart'; import 'package:playmaker/pages/PlacarPage.dart';
import '../controllers/game_controller.dart'; import 'package:playmaker/classe/theme.dart';
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../controllers/game_controller.dart'; import '../controllers/game_controller.dart';
import '../models/game_model.dart'; import '../models/game_model.dart';
import '../utils/size_extension.dart'; // 👇 NOVO SUPERPODER AQUI TAMBÉM! import '../utils/size_extension.dart';
// --- CARD DE EXIBIÇÃO DO JOGO --- // --- CARD DE EXIBIÇÃO DO JOGO ---
class GameResultCard extends StatelessWidget { class GameResultCard extends StatelessWidget {
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season; final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
final String? myTeamLogo, opponentTeamLogo; final String? myTeamLogo, opponentTeamLogo;
final double sf;
const GameResultCard({ const GameResultCard({
super.key, required this.gameId, required this.myTeam, required this.opponentTeam, super.key, required this.gameId, required this.myTeam, required this.opponentTeam,
required this.myScore, required this.opponentScore, required this.status, required this.season, required this.myScore, required this.opponentScore, required this.status, required this.season,
this.myTeamLogo, this.opponentTeamLogo, this.myTeamLogo, this.opponentTeamLogo, required this.sf,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bgColor = Theme.of(context).cardTheme.color;
final textColor = Theme.of(context).colorScheme.onSurface;
return Container( return Container(
margin: EdgeInsets.only(bottom: 16 * context.sf), margin: EdgeInsets.only(bottom: 16 * sf),
padding: EdgeInsets.all(16 * context.sf), padding: EdgeInsets.all(16 * sf),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20 * context.sf), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * context.sf)]), decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(20 * sf),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: _buildTeamInfo(context, myTeam, const Color(0xFFE74C3C), myTeamLogo)), Expanded(child: _buildTeamInfo(myTeam, AppTheme.primaryRed, myTeamLogo, sf, textColor)),
_buildScoreCenter(context, gameId), _buildScoreCenter(context, gameId, sf, textColor),
Expanded(child: _buildTeamInfo(context, opponentTeam, Colors.black87, opponentTeamLogo)), Expanded(child: _buildTeamInfo(opponentTeam, Colors.grey.shade600, opponentTeamLogo, sf, textColor)),
], ],
), ),
); );
} }
Widget _buildTeamInfo(BuildContext context, String name, Color color, String? logoUrl) { Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) {
return Column( return Column(
children: [ children: [
CircleAvatar(radius: 24 * context.sf, backgroundColor: color, backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * context.sf) : null), CircleAvatar(
SizedBox(height: 6 * context.sf), radius: 24 * sf,
Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2), backgroundColor: color,
backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null,
child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * sf) : null,
),
SizedBox(height: 6 * sf),
Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf, color: textColor), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2),
], ],
); );
} }
Widget _buildScoreCenter(BuildContext context, String id) { Widget _buildScoreCenter(BuildContext context, String id, double sf, Color textColor) {
return Column( return Column(
children: [ children: [
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_scoreBox(context, myScore, Colors.green), _scoreBox(myScore, AppTheme.successGreen, sf),
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * context.sf)), Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf, color: textColor)),
_scoreBox(context, opponentScore, Colors.grey), _scoreBox(opponentScore, Colors.grey, sf),
], ],
), ),
SizedBox(height: 10 * context.sf), SizedBox(height: 10 * sf),
TextButton.icon( TextButton.icon(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))), 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)), icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: AppTheme.primaryRed),
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * context.sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)), label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: AppTheme.primaryRed, 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), style: TextButton.styleFrom(backgroundColor: AppTheme.primaryRed.withOpacity(0.1), padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), visualDensity: VisualDensity.compact),
), ),
SizedBox(height: 6 * context.sf), SizedBox(height: 6 * sf),
Text(status, style: TextStyle(fontSize: 12 * context.sf, color: Colors.blue, fontWeight: FontWeight.bold)), Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)),
], ],
); );
} }
Widget _scoreBox(BuildContext context, String pts, Color c) => Container( Widget _scoreBox(String pts, Color c, double sf) => Container(
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf, vertical: 6 * context.sf), padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * context.sf)), decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)),
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)), child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
); );
} }
@@ -79,8 +93,9 @@ class GameResultCard extends StatelessWidget {
class CreateGameDialogManual extends StatefulWidget { class CreateGameDialogManual extends StatefulWidget {
final TeamController teamController; final TeamController teamController;
final GameController gameController; final GameController gameController;
final double sf;
const CreateGameDialogManual({super.key, required this.teamController, required this.gameController}); const CreateGameDialogManual({super.key, required this.teamController, required this.gameController, required this.sf});
@override @override
State<CreateGameDialogManual> createState() => _CreateGameDialogManualState(); State<CreateGameDialogManual> createState() => _CreateGameDialogManualState();
@@ -106,24 +121,29 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), backgroundColor: Theme.of(context).colorScheme.surface,
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * widget.sf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * widget.sf, color: Theme.of(context).colorScheme.onSurface)),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField(controller: _seasonController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * context.sf))), TextField(
SizedBox(height: 15 * context.sf), controller: _seasonController,
style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * widget.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * widget.sf))
),
SizedBox(height: 15 * widget.sf),
_buildSearch(context, "Minha Equipa", _myTeamController), _buildSearch(context, "Minha Equipa", _myTeamController),
Padding(padding: EdgeInsets.symmetric(vertical: 10 * context.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * context.sf))), Padding(padding: EdgeInsets.symmetric(vertical: 10 * widget.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * widget.sf))),
_buildSearch(context, "Adversário", _opponentController), _buildSearch(context, "Adversário", _opponentController),
], ],
), ),
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * context.sf))), TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)), style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * widget.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)),
onPressed: _isLoading ? null : () async { onPressed: _isLoading ? null : () async {
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) { if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
setState(() => _isLoading = true); setState(() => _isLoading = true);
@@ -135,7 +155,7 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
} }
} }
}, },
child: _isLoading ? SizedBox(width: 20 * context.sf, height: 20 * context.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), child: _isLoading ? SizedBox(width: 20 * widget.sf, height: 20 * widget.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * widget.sf)),
), ),
], ],
); );
@@ -157,9 +177,10 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
return Align( return Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Material( child: Material(
elevation: 4.0, borderRadius: BorderRadius.circular(8 * context.sf), color: Theme.of(context).colorScheme.surface,
elevation: 4.0, borderRadius: BorderRadius.circular(8 * widget.sf),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 250 * context.sf, maxWidth: MediaQuery.of(context).size.width * 0.7), constraints: BoxConstraints(maxHeight: 250 * widget.sf, maxWidth: MediaQuery.of(context).size.width * 0.7),
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
@@ -167,8 +188,8 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
final String name = option['name'].toString(); final String name = option['name'].toString();
final String? imageUrl = option['image_url']; final String? imageUrl = option['image_url'];
return ListTile( return ListTile(
leading: CircleAvatar(radius: 20 * 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), leading: CircleAvatar(radius: 20 * widget.sf, backgroundColor: Colors.grey.withOpacity(0.2), backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * widget.sf) : null),
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface)),
onTap: () { onSelected(option); }, onTap: () { onSelected(option); },
); );
}, },
@@ -181,8 +202,9 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) txtCtrl.text = controller.text; if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) txtCtrl.text = controller.text;
txtCtrl.addListener(() { controller.text = txtCtrl.text; }); txtCtrl.addListener(() { controller.text = txtCtrl.text; });
return TextField( return TextField(
controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * context.sf), controller: txtCtrl, focusNode: node,
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * context.sf), prefixIcon: Icon(Icons.search, size: 20 * context.sf), border: const OutlineInputBorder()), style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * widget.sf), prefixIcon: Icon(Icons.search, size: 20 * widget.sf, color: AppTheme.primaryRed)),
); );
}, },
); );
@@ -202,20 +224,24 @@ class GamePage extends StatefulWidget {
class _GamePageState extends State<GamePage> { class _GamePageState extends State<GamePage> {
final GameController gameController = GameController(); final GameController gameController = GameController();
final TeamController teamController = TeamController(); final TeamController teamController = TeamController();
String selectedSeason = 'Todas';
String selectedTeam = 'Todas';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isFilterActive = selectedSeason != 'Todas' || selectedTeam != 'Todas';
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)), title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)),
backgroundColor: Colors.white, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 0, elevation: 0,
actions: [ actions: [
Padding( Padding(
padding: EdgeInsets.only(right: 8.0 * context.sf), padding: EdgeInsets.only(right: 8.0 * context.sf),
child: IconButton( child: IconButton(
icon: Icon(isFilterActive ? Icons.filter_list_alt : Icons.filter_list, color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87, size: 26 * context.sf), icon: Icon(isFilterActive ? Icons.filter_list_alt : Icons.filter_list, color: isFilterActive ? AppTheme.primaryRed : Theme.of(context).colorScheme.onSurface, size: 26 * context.sf),
onPressed: () => _showFilterPopup(context), onPressed: () => _showFilterPopup(context),
), ),
) )
@@ -225,40 +251,28 @@ class _GamePageState extends State<GamePage> {
stream: teamController.teamsStream, stream: teamController.teamsStream,
builder: (context, teamSnapshot) { builder: (context, teamSnapshot) {
final List<Map<String, dynamic>> teamsList = teamSnapshot.data ?? []; final List<Map<String, dynamic>> teamsList = teamSnapshot.data ?? [];
// 2º STREAM: Lemos os jogos
return StreamBuilder<List<Game>>( return StreamBuilder<List<Game>>(
stream: gameController.gamesStream, stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason),
builder: (context, gameSnapshot) { builder: (context, gameSnapshot) {
if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) return const Center(child: CircularProgressIndicator()); if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) return const Center(child: CircularProgressIndicator());
if (gameSnapshot.hasError) return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * context.sf))); if (gameSnapshot.hasError) return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * context.sf, color: Theme.of(context).colorScheme.onSurface)));
if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) { if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) {
return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.search_off, size: 48 * context.sf, color: Colors.grey.shade300), SizedBox(height: 10 * context.sf), Text("Nenhum jogo encontrado.", style: TextStyle(fontSize: 14 * context.sf, color: Colors.grey.shade600))])); return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.search_off, size: 48 * context.sf, color: Colors.grey.withOpacity(0.3)), SizedBox(height: 10 * context.sf), Text("Nenhum jogo encontrado.", style: TextStyle(fontSize: 14 * context.sf, color: Colors.grey))]));
} }
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.all(16), padding: EdgeInsets.all(16 * context.sf),
itemCount: gameSnapshot.data!.length, itemCount: gameSnapshot.data!.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final game = gameSnapshot.data![index]; final game = gameSnapshot.data![index];
String? myLogo, oppLogo;
// --- LÓGICA PARA ENCONTRAR A IMAGEM PELO NOME ---
String? myLogo;
String? oppLogo;
for (var team in teamsList) { for (var team in teamsList) {
if (team['name'] == game.myTeam) { if (team['name'] == game.myTeam) myLogo = team['image_url'];
myLogo = team['image_url']; if (team['name'] == game.opponentTeam) oppLogo = team['image_url'];
}
if (team['name'] == game.opponentTeam) {
oppLogo = team['image_url'];
}
} }
// Agora já passamos as imagens para o cartão!
return GameResultCard( return GameResultCard(
gameId: game.id, myTeam: game.myTeam, opponentTeam: game.opponentTeam, myScore: game.myScore, 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, opponentScore: game.opponentScore, status: game.status, season: game.season, myTeamLogo: myLogo, opponentTeamLogo: oppLogo,
sf: context.sf,
); );
}, },
); );
@@ -267,49 +281,53 @@ class _GamePageState extends State<GamePage> {
}, },
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'add_game_btn', // 👇 A MÁGICA ESTÁ AQUI TAMBÉM! heroTag: 'add_game_btn',
backgroundColor: const Color(0xFFE74C3C), backgroundColor: AppTheme.primaryRed,
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf), child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
onPressed: () => showDialog(context: context, builder: (context) => CreateGameDialogManual(teamController: teamController, gameController: gameController)), onPressed: () => showDialog(context: context, builder: (context) => CreateGameDialogManual(teamController: teamController, gameController: gameController, sf: context.sf)),
), ),
); );
} }
void _showCreateDialog(BuildContext context) { void _showFilterPopup(BuildContext context) {
String tempSeason = selectedSeason;
String tempTeam = selectedTeam;
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setPopupState) { builder: (context, setPopupState) {
return AlertDialog( return AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf)), Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * context.sf, color: Theme.of(context).colorScheme.onSurface)),
IconButton(icon: const Icon(Icons.close, color: Colors.grey), onPressed: () => Navigator.pop(context), padding: EdgeInsets.zero, constraints: const BoxConstraints()) IconButton(icon: const Icon(Icons.close, color: Colors.grey), onPressed: () => Navigator.pop(context), padding: EdgeInsets.zero, constraints: const BoxConstraints())
], ],
), ),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text("Temporada", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), Text("Temporada", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * context.sf), SizedBox(height: 6 * context.sf),
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)), padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(10 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))),
child: DropdownButtonHideUnderline( child: DropdownButtonHideUnderline(
child: DropdownButton<String>( child: DropdownButton<String>(
isExpanded: true, value: tempSeason, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold), dropdownColor: Theme.of(context).colorScheme.surface,
isExpanded: true, value: tempSeason, style: TextStyle(fontSize: 14 * context.sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold),
items: ['Todas', '2024/25', '2025/26'].map((String value) => DropdownMenuItem<String>(value: value, child: Text(value))).toList(), items: ['Todas', '2024/25', '2025/26'].map((String value) => DropdownMenuItem<String>(value: value, child: Text(value))).toList(),
onChanged: (newValue) => setPopupState(() => tempSeason = newValue!), onChanged: (newValue) => setPopupState(() => tempSeason = newValue!),
), ),
), ),
), ),
SizedBox(height: 20 * context.sf), SizedBox(height: 20 * context.sf),
Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), Text("Equipa", style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)),
SizedBox(height: 6 * context.sf), SizedBox(height: 6 * context.sf),
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * context.sf)), padding: EdgeInsets.symmetric(horizontal: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(10 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))),
child: StreamBuilder<List<Map<String, dynamic>>>( child: StreamBuilder<List<Map<String, dynamic>>>(
stream: teamController.teamsStream, stream: teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -318,7 +336,8 @@ class _GamePageState extends State<GamePage> {
if (!teamNames.contains(tempTeam)) tempTeam = 'Todas'; if (!teamNames.contains(tempTeam)) tempTeam = 'Todas';
return DropdownButtonHideUnderline( return DropdownButtonHideUnderline(
child: DropdownButton<String>( child: DropdownButton<String>(
isExpanded: true, value: tempTeam, style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87, fontWeight: FontWeight.bold), dropdownColor: Theme.of(context).colorScheme.surface,
isExpanded: true, value: tempTeam, style: TextStyle(fontSize: 14 * context.sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold),
items: teamNames.map((String value) => DropdownMenuItem<String>(value: value, child: Text(value, overflow: TextOverflow.ellipsis))).toList(), items: teamNames.map((String value) => DropdownMenuItem<String>(value: value, child: Text(value, overflow: TextOverflow.ellipsis))).toList(),
onChanged: (newValue) => setPopupState(() => tempTeam = newValue!), onChanged: (newValue) => setPopupState(() => tempTeam = newValue!),
), ),
@@ -330,7 +349,7 @@ class _GamePageState extends State<GamePage> {
), ),
actions: [ actions: [
TextButton(onPressed: () { setState(() { selectedSeason = 'Todas'; selectedTeam = 'Todas'; }); Navigator.pop(context); }, child: Text('LIMPAR', style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey))), TextButton(onPressed: () { setState(() { selectedSeason = 'Todas'; selectedTeam = 'Todas'; }); Navigator.pop(context); }, child: Text('LIMPAR', style: TextStyle(fontSize: 12 * context.sf, color: Colors.grey))),
ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), onPressed: () { setState(() { selectedSeason = tempSeason; selectedTeam = tempTeam; }); Navigator.pop(context); }, child: Text('APLICAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * context.sf))), ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), onPressed: () { setState(() { selectedSeason = tempSeason; selectedTeam = tempTeam; }); Navigator.pop(context); }, child: Text('APLICAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * context.sf))),
], ],
); );
} }

View File

@@ -8,6 +8,8 @@ import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:playmaker/pages/status_page.dart'; import 'package:playmaker/pages/status_page.dart';
import '../utils/size_extension.dart'; import '../utils/size_extension.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
// 👇 Importa o ficheiro onde meteste o StatCard e o SportGrid
// import 'home_widgets.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@@ -38,7 +40,7 @@ class _HomeScreenState extends State<HomeScreen> {
]; ];
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, // Fundo dinâmico backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)), title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)),
backgroundColor: AppTheme.primaryRed, backgroundColor: AppTheme.primaryRed,
@@ -46,7 +48,6 @@ class _HomeScreenState extends State<HomeScreen> {
leading: IconButton( leading: IconButton(
icon: Icon(Icons.person, size: 24 * context.sf), icon: Icon(Icons.person, size: 24 * context.sf),
onPressed: () { onPressed: () {
// 👇 MAGIA ACONTECE AQUI 👇
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => const SettingsScreen()), MaterialPageRoute(builder: (context) => const SettingsScreen()),
@@ -80,7 +81,7 @@ class _HomeScreenState extends State<HomeScreen> {
void _showTeamSelector(BuildContext context) { void _showTeamSelector(BuildContext context) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
backgroundColor: Theme.of(context).colorScheme.surface, // Fundo dinâmico backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf))),
builder: (context) { builder: (context) {
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
@@ -96,7 +97,7 @@ class _HomeScreenState extends State<HomeScreen> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final team = teams[index]; final team = teams[index];
return ListTile( return ListTile(
title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), // Texto dinâmico title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
onTap: () { onTap: () {
setState(() { setState(() {
_selectedTeamId = team['id']; _selectedTeamId = team['id'];
@@ -195,11 +196,55 @@ class _HomeScreenState extends State<HomeScreen> {
Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: textColor)), Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: textColor)),
SizedBox(height: 16 * context.sf), SizedBox(height: 16 * context.sf),
// 👇 AQUI ESTÁ O NOVO CARTÃO VAZIO PARA QUANDO NÃO HÁ EQUIPA 👇
_selectedTeamName == "Selecionar Equipa" _selectedTeamName == "Selecionar Equipa"
? Container( ? Container(
padding: EdgeInsets.all(20 * context.sf), width: double.infinity,
alignment: Alignment.center, padding: EdgeInsets.all(24.0 * context.sf),
child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf)), decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color ?? Colors.white,
borderRadius: BorderRadius.circular(16 * context.sf),
border: Border.all(color: Colors.grey.withOpacity(0.1)),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4)),
],
),
child: Column(
children: [
Container(
padding: EdgeInsets.all(18 * context.sf),
decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.08), shape: BoxShape.circle),
child: Icon(Icons.shield_outlined, color: AppTheme.primaryRed, size: 42 * context.sf),
),
SizedBox(height: 20 * context.sf),
Text(
"Nenhuma Equipa Ativa",
style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface),
),
SizedBox(height: 8 * context.sf),
Text(
"Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13 * context.sf, color: Colors.grey.shade600, height: 1.4),
),
SizedBox(height: 24 * context.sf),
SizedBox(
width: double.infinity,
height: 48 * context.sf,
child: ElevatedButton.icon(
onPressed: () => _showTeamSelector(context),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryRed,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf)),
),
icon: Icon(Icons.touch_app, size: 20 * context.sf),
label: Text("Selecionar Agora", style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.bold)),
),
),
],
),
) )
: StreamBuilder<List<Map<String, dynamic>>>( : StreamBuilder<List<Map<String, dynamic>>>(
stream: _supabase.from('games').stream(primaryKey: ['id']) stream: _supabase.from('games').stream(primaryKey: ['id'])
@@ -248,17 +293,10 @@ class _HomeScreenState extends State<HomeScreen> {
if (myScore < oppScore) result = 'D'; if (myScore < oppScore) result = 'D';
return _buildGameHistoryCard( return _buildGameHistoryCard(
context: context, context: context, opponent: opponent, result: result,
opponent: opponent, myScore: myScore, oppScore: oppScore, date: date,
result: result, topPts: game['top_pts_name'] ?? '---', topAst: game['top_ast_name'] ?? '---',
myScore: myScore, topRbs: game['top_rbs_name'] ?? '---', topDef: game['top_def_name'] ?? '---', mvp: game['mvp_name'] ?? '---',
oppScore: oppScore,
date: date,
topPts: game['top_pts_name'] ?? '---',
topAst: game['top_ast_name'] ?? '---',
topRbs: game['top_rbs_name'] ?? '---',
topDef: game['top_def_name'] ?? '---',
mvp: game['mvp_name'] ?? '---',
); );
}).toList(), }).toList(),
); );

View File

@@ -3,7 +3,7 @@ import 'package:playmaker/screens/team_stats_page.dart';
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../models/team_model.dart'; import '../models/team_model.dart';
import '../utils/size_extension.dart'; // 👇 IMPORTANTE: O TEU NOVO SUPERPODER import '../utils/size_extension.dart';
class TeamsPage extends StatefulWidget { class TeamsPage extends StatefulWidget {
const TeamsPage({super.key}); const TeamsPage({super.key});
@@ -26,7 +26,6 @@ class _TeamsPageState extends State<TeamsPage> {
super.dispose(); super.dispose();
} }
// --- POPUP DE FILTROS ---
void _showFilterDialog(BuildContext context) { void _showFilterDialog(BuildContext context) {
showDialog( showDialog(
context: context, context: context,
@@ -34,14 +33,14 @@ class _TeamsPageState extends State<TeamsPage> {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setModalState) { builder: (context, setModalState) {
return AlertDialog( return AlertDialog(
backgroundColor: const Color(0xFF2C3E50), backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text("Filtros de pesquisa", style: TextStyle(color: Colors.white, fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), Text("Filtros de pesquisa", style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 18 * context.sf, fontWeight: FontWeight.bold)),
IconButton( IconButton(
icon: Icon(Icons.close, color: Colors.white, size: 20 * context.sf), icon: Icon(Icons.close, color: Colors.grey, size: 20 * context.sf),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
) )
], ],
@@ -49,12 +48,11 @@ class _TeamsPageState extends State<TeamsPage> {
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Divider(color: Colors.white24), Divider(color: Colors.grey.withOpacity(0.2)),
SizedBox(height: 16 * context.sf), SizedBox(height: 16 * context.sf),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Coluna Temporada
Expanded( Expanded(
child: _buildPopupColumn( child: _buildPopupColumn(
title: "TEMPORADA", title: "TEMPORADA",
@@ -66,8 +64,7 @@ class _TeamsPageState extends State<TeamsPage> {
}, },
), ),
), ),
const SizedBox(width: 20), SizedBox(width: 20 * context.sf),
// Coluna Ordenar
Expanded( Expanded(
child: _buildPopupColumn( child: _buildPopupColumn(
title: "ORDENAR POR", title: "ORDENAR POR",
@@ -86,7 +83,7 @@ class _TeamsPageState extends State<TeamsPage> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text("CONCLUÍDO", style: TextStyle(color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold, fontSize: 14 * context.sf)), child: Text("CONCLUÍDO", style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * context.sf)),
), ),
], ],
); );
@@ -96,28 +93,24 @@ class _TeamsPageState extends State<TeamsPage> {
); );
} }
Widget _buildPopupColumn({ Widget _buildPopupColumn({required String title, required List<String> options, required String currentValue, required Function(String) onSelect}) {
required String title,
required List<String> options,
required String currentValue,
required Function(String) onSelect,
}) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, style: const TextStyle(color: Colors.grey, fontSize: 11, fontWeight: FontWeight.bold)), Text(title, style: TextStyle(color: Colors.grey, fontSize: 11 * context.sf, fontWeight: FontWeight.bold)),
const SizedBox(height: 12), SizedBox(height: 12 * context.sf),
...options.map((opt) { ...options.map((opt) {
final isSelected = currentValue == opt; final isSelected = currentValue == opt;
return InkWell( return InkWell(
onTap: () => onSelect(opt), onTap: () => onSelect(opt),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: EdgeInsets.symmetric(vertical: 8.0 * context.sf),
child: Text( child: Text(
opt, opt,
style: TextStyle( style: TextStyle(
color: isSelected ? const Color(0xFFE74C3C) : Colors.white70, color: isSelected ? AppTheme.primaryRed : Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 14 * context.sf,
), ),
), ),
), ),
@@ -133,11 +126,11 @@ class _TeamsPageState extends State<TeamsPage> {
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
title: Text("Minhas Equipas", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)), title: Text("Minhas Equipas", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * context.sf)),
backgroundColor: const Color(0xFFF5F7FA), backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 0, elevation: 0,
actions: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.filter_list, color: const Color(0xFFE74C3C), size: 24 * context.sf), icon: Icon(Icons.filter_list, color: AppTheme.primaryRed, size: 24 * context.sf),
onPressed: () => _showFilterDialog(context), onPressed: () => _showFilterDialog(context),
), ),
], ],
@@ -149,8 +142,8 @@ class _TeamsPageState extends State<TeamsPage> {
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'add_team_btn', // 👇 A MÁGICA ESTÁ AQUI! heroTag: 'add_team_btn',
backgroundColor: const Color(0xFFE74C3C), backgroundColor: AppTheme.primaryRed,
child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf), child: Icon(Icons.add, color: Colors.white, size: 24 * context.sf),
onPressed: () => _showCreateDialog(context), onPressed: () => _showCreateDialog(context),
), ),
@@ -159,17 +152,17 @@ class _TeamsPageState extends State<TeamsPage> {
Widget _buildSearchBar() { Widget _buildSearchBar() {
return Padding( return Padding(
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0 * context.sf),
child: TextField( child: TextField(
controller: _searchController, controller: _searchController,
onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()), onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()),
style: TextStyle(fontSize: 16 * context.sf), style: TextStyle(fontSize: 16 * context.sf, color: Theme.of(context).colorScheme.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Pesquisar equipa...', hintText: 'Pesquisar equipa...',
hintStyle: TextStyle(fontSize: 16 * context.sf), hintStyle: TextStyle(fontSize: 16 * context.sf, color: Colors.grey),
prefixIcon: Icon(Icons.search, color: const Color(0xFFE74C3C), size: 22 * context.sf), prefixIcon: Icon(Icons.search, color: AppTheme.primaryRed, size: 22 * context.sf),
filled: true, filled: true,
fillColor: Colors.white, fillColor: Theme.of(context).colorScheme.surface, // 👇 Adapta-se ao Dark Mode
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * context.sf), borderSide: BorderSide.none), border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * context.sf), borderSide: BorderSide.none),
), ),
), ),
@@ -180,51 +173,30 @@ class _TeamsPageState extends State<TeamsPage> {
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: controller.teamsStream, stream: controller.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator()); if (snapshot.connectionState == ConnectionState.waiting) return Center(child: CircularProgressIndicator(color: AppTheme.primaryRed));
if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * context.sf))); if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * context.sf, color: Theme.of(context).colorScheme.onSurface)));
var data = List<Map<String, dynamic>>.from(snapshot.data!); var data = List<Map<String, dynamic>>.from(snapshot.data!);
// --- 1. FILTROS --- if (_selectedSeason != 'Todas') data = data.where((t) => t['season'] == _selectedSeason).toList();
if (_selectedSeason != 'Todas') { if (_searchQuery.isNotEmpty) data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList();
data = data.where((t) => t['season'] == _selectedSeason).toList();
}
if (_searchQuery.isNotEmpty) {
data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList();
}
// --- 2. ORDENAÇÃO (FAVORITOS PRIMEIRO) ---
data.sort((a, b) { data.sort((a, b) {
// Apanhar o estado de favorito (tratando null como false)
bool favA = a['is_favorite'] ?? false; bool favA = a['is_favorite'] ?? false;
bool favB = b['is_favorite'] ?? false; bool favB = b['is_favorite'] ?? false;
if (favA && !favB) return -1;
// REGRA 1: Favoritos aparecem sempre primeiro if (!favA && favB) return 1;
if (favA && !favB) return -1; // A sobe if (_currentSort == 'Nome') return a['name'].toString().compareTo(b['name'].toString());
if (!favA && favB) return 1; // B sobe else return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString());
// REGRA 2: Se o estado de favorito for igual, aplica o filtro do utilizador
if (_currentSort == 'Nome') {
return a['name'].toString().compareTo(b['name'].toString());
} else { // Recentes
return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString());
}
}); });
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf),
itemCount: data.length, itemCount: data.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final team = Team.fromMap(data[index]); final team = Team.fromMap(data[index]);
// Navegação para estatísticas
return GestureDetector( return GestureDetector(
onTap: () { onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))),
Navigator.push(
context,
MaterialPageRoute(builder: (context) => TeamStatsPage(team: team)),
);
},
child: TeamCard( child: TeamCard(
team: team, team: team,
controller: controller, controller: controller,
@@ -239,7 +211,7 @@ class _TeamsPageState extends State<TeamsPage> {
} }
void _showCreateDialog(BuildContext context) { void _showCreateDialog(BuildContext context) {
showDialog(context: context, builder: (context) => CreateTeamDialog(onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl))); showDialog(context: context, builder: (context) => CreateTeamDialog(sf: context.sf, onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl)));
} }
} }
@@ -248,73 +220,140 @@ class TeamCard extends StatelessWidget {
final Team team; final Team team;
final TeamController controller; final TeamController controller;
final VoidCallback onFavoriteTap; final VoidCallback onFavoriteTap;
final double sf;
const TeamCard({super.key, required this.team, required this.controller, required this.onFavoriteTap}); const TeamCard({
super.key,
required this.team,
required this.controller,
required this.onFavoriteTap,
required this.sf,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
color: Colors.white, elevation: 3, margin: EdgeInsets.only(bottom: 12 * context.sf), final textColor = Theme.of(context).colorScheme.onSurface;
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
child: ListTile( return Container(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 8 * context.sf), margin: EdgeInsets.only(bottom: 12 * sf),
leading: Stack( decoration: BoxDecoration(
clipBehavior: Clip.none, color: bgColor,
children: [ borderRadius: BorderRadius.circular(15 * sf),
CircleAvatar( border: Border.all(color: Colors.grey.withOpacity(0.15)),
radius: 28 * context.sf, backgroundColor: Colors.grey[200], boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10 * sf)]
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, child: Material(
), color: Colors.transparent,
Positioned( borderRadius: BorderRadius.circular(15 * sf),
left: -15 * context.sf, top: -10 * context.sf, child: ListTile(
child: IconButton( contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * 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)]), leading: Stack(
onPressed: onFavoriteTap, clipBehavior: Clip.none,
),
),
],
),
title: Text(team.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * context.sf), overflow: TextOverflow.ellipsis),
subtitle: Padding(
padding: EdgeInsets.only(top: 6.0 * context.sf),
child: Row(
children: [ children: [
Icon(Icons.groups_outlined, size: 16 * context.sf, color: Colors.grey), CircleAvatar(
SizedBox(width: 4 * context.sf), radius: 28 * sf,
StreamBuilder<int>( backgroundColor: Colors.grey.withOpacity(0.2),
stream: controller.getPlayerCountStream(team.id), backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
initialData: 0, ? NetworkImage(team.imageUrl)
builder: (context, snapshot) { : null,
final count = snapshot.data ?? 0; child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
return Text("$count Jogs.", style: TextStyle(color: count > 0 ? Colors.green[700] : Colors.orange, fontWeight: FontWeight.bold, fontSize: 13 * context.sf)); ? Text(
}, team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
style: TextStyle(fontSize: 24 * sf),
)
: null,
),
Positioned(
left: -15 * sf,
top: -10 * sf,
child: IconButton(
icon: Icon(
team.isFavorite ? Icons.star : Icons.star_border,
color: team.isFavorite ? AppTheme.warningAmber : Theme.of(context).colorScheme.onSurface.withOpacity(0.2),
size: 28 * sf,
shadows: [
Shadow(color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), blurRadius: 4 * sf),
],
),
onPressed: onFavoriteTap,
),
),
],
),
title: Text(
team.name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf, color: textColor),
overflow: TextOverflow.ellipsis,
),
subtitle: Padding(
padding: EdgeInsets.only(top: 6.0 * sf),
child: Row(
children: [
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
SizedBox(width: 4 * sf),
StreamBuilder<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 ? AppTheme.successGreen : AppTheme.warningAmber, // 👇 Usando cores do tema
fontWeight: FontWeight.bold,
fontSize: 13 * sf,
),
);
},
),
SizedBox(width: 8 * sf),
Expanded(
child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * sf), overflow: TextOverflow.ellipsis),
),
],
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'Ver Estatísticas',
icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * sf),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))),
),
IconButton(
tooltip: 'Eliminar Equipa',
icon: Icon(Icons.delete_outline, color: AppTheme.primaryRed, size: 24 * sf),
onPressed: () => _confirmDelete(context, sf, bgColor, textColor),
), ),
SizedBox(width: 8 * context.sf),
Expanded(child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * context.sf), overflow: TextOverflow.ellipsis)),
], ],
), ),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(tooltip: 'Ver Estatísticas', icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * context.sf), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team)))),
IconButton(tooltip: 'Eliminar Equipa', icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * context.sf), onPressed: () => _confirmDelete(context)),
],
), ),
), ),
); );
} }
void _confirmDelete(BuildContext context) { void _confirmDelete(BuildContext context, double sf, Color cardColor, Color textColor) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), backgroundColor: cardColor,
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * context.sf)), surfaceTintColor: Colors.transparent,
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold, color: textColor)),
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * sf, color: textColor)),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))), TextButton(
TextButton(onPressed: () { controller.deleteTeam(team.id); Navigator.pop(context); }, child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * context.sf))), onPressed: () => Navigator.pop(context),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf, color: Colors.grey)),
),
TextButton(
onPressed: () {
controller.deleteTeam(team.id);
Navigator.pop(context);
},
child: Text('Eliminar', style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf)),
),
], ],
), ),
); );
@@ -324,7 +363,9 @@ class TeamCard extends StatelessWidget {
// --- DIALOG DE CRIAÇÃO --- // --- DIALOG DE CRIAÇÃO ---
class CreateTeamDialog extends StatefulWidget { class CreateTeamDialog extends StatefulWidget {
final Function(String name, String season, String imageUrl) onConfirm; final Function(String name, String season, String imageUrl) onConfirm;
const CreateTeamDialog({super.key, required this.onConfirm}); final double sf;
const CreateTeamDialog({super.key, required this.onConfirm, required this.sf});
@override @override
State<CreateTeamDialog> createState() => _CreateTeamDialogState(); State<CreateTeamDialog> createState() => _CreateTeamDialogState();
@@ -338,31 +379,33 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)), backgroundColor: Theme.of(context).colorScheme.surface,
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)),
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField(controller: _nameController, style: TextStyle(fontSize: 14 * context.sf), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * context.sf)), textCapitalization: TextCapitalization.words), TextField(controller: _nameController, style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * widget.sf)), textCapitalization: TextCapitalization.words),
SizedBox(height: 15 * context.sf), SizedBox(height: 15 * widget.sf),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * context.sf)), dropdownColor: Theme.of(context).colorScheme.surface,
style: TextStyle(fontSize: 14 * context.sf, color: Colors.black87), value: _selectedSeason, decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * widget.sf)),
style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface),
items: ['2023/24', '2024/25', '2025/26'].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), items: ['2023/24', '2024/25', '2025/26'].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
onChanged: (val) => setState(() => _selectedSeason = val!), onChanged: (val) => setState(() => _selectedSeason = val!),
), ),
SizedBox(height: 15 * context.sf), SizedBox(height: 15 * widget.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))), TextField(controller: _imageController, style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration(labelText: 'URL Imagem ou Emoji', labelStyle: TextStyle(fontSize: 14 * widget.sf), hintText: 'Ex: 🏀 ou https://...', hintStyle: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))),
], ],
), ),
), ),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * context.sf))), TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 10 * context.sf)), style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)),
onPressed: () { if (_nameController.text.trim().isNotEmpty) { widget.onConfirm(_nameController.text.trim(), _selectedSeason, _imageController.text.trim()); Navigator.pop(context); } }, onPressed: () { if (_nameController.text.trim().isNotEmpty) { widget.onConfirm(_nameController.text.trim(), _selectedSeason, _imageController.text.trim()); Navigator.pop(context); } },
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * context.sf)), child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)),
), ),
], ],
); );

View File

@@ -1 +1,11 @@
// TODO Implement this library. import 'package:flutter/material.dart';
import 'dart:math' as math;
extension SizeExtension on BuildContext {
double get sf {
final double wScreen = MediaQuery.of(this).size.width;
final double hScreen = MediaQuery.of(this).size.height;
// Ajusta a escala baseada no ecrã (muda os valores 1150/720 conforme a tua calibração)
return math.min(wScreen / 1150, hScreen / 720);
}
}

View File

@@ -1,27 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/pages/PlacarPage.dart'; import 'package:playmaker/pages/PlacarPage.dart';
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA!
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../controllers/game_controller.dart'; import '../controllers/game_controller.dart';
class GameResultCard extends StatelessWidget { class GameResultCard extends StatelessWidget {
final String gameId; final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
final String myTeam, opponentTeam, myScore, opponentScore, status, season; final String? myTeamLogo, opponentTeamLogo;
final String? myTeamLogo; final double sf;
final String? opponentTeamLogo;
final double sf; // NOVA VARIÁVEL DE ESCALA
const GameResultCard({ const GameResultCard({
super.key, super.key, required this.gameId, required this.myTeam, required this.opponentTeam,
required this.gameId, required this.myScore, required this.opponentScore, required this.status, required this.season,
required this.myTeam, this.myTeamLogo, this.opponentTeamLogo, required this.sf,
required this.opponentTeam,
required this.myScore,
required this.opponentScore,
required this.status,
required this.season,
this.myTeamLogo,
this.opponentTeamLogo,
required this.sf, // OBRIGATÓRIO RECEBER A ESCALA
}); });
@override @override
@@ -31,291 +22,76 @@ class GameResultCard extends StatelessWidget {
final textColor = Theme.of(context).colorScheme.onSurface; final textColor = Theme.of(context).colorScheme.onSurface;
return Container( return Container(
margin: const EdgeInsets.only(bottom: 16), margin: EdgeInsets.only(bottom: 16 * sf),
padding: const EdgeInsets.all(16), padding: EdgeInsets.all(16 * sf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: bgColor, // Usa a cor do tema
borderRadius: BorderRadius.circular(20 * sf), borderRadius: BorderRadius.circular(20 * sf),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)], boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)],
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, sf)), Expanded(child: _buildTeamInfo(myTeam, AppTheme.primaryRed, myTeamLogo, sf, textColor)), // Usa o primaryRed
_buildScoreCenter(context, gameId, sf), _buildScoreCenter(context, gameId, sf),
Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, sf)), Expanded(child: _buildTeamInfo(opponentTeam, textColor, opponentTeamLogo, sf, textColor)),
], ],
), ),
); );
} }
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf) { Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) {
return Column( return Column(
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 24 * sf, // Ajuste do tamanho do logo radius: 24 * sf,
backgroundColor: color, backgroundColor: color,
backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null,
? NetworkImage(logoUrl) child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * sf) : null,
: null,
child: (logoUrl == null || logoUrl.isEmpty)
? Icon(Icons.shield, color: Colors.white, size: 24 * sf)
: null,
), ),
const SizedBox(height: 4), SizedBox(height: 6 * sf),
Text(name, Text(name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf, color: textColor), // Adapta à noite/dia
textAlign: TextAlign.center, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2,
overflow: TextOverflow.ellipsis,
maxLines: 2, // Permite 2 linhas para nomes compridos não cortarem
), ),
], ],
); );
} }
Widget _buildScoreCenter(BuildContext context, String id, double sf) { Widget _buildScoreCenter(BuildContext context, String id, double sf) {
final textColor = Theme.of(context).colorScheme.onSurface;
return Column( return Column(
children: [ children: [
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_scoreBox(myScore, Colors.green, sf), _scoreBox(myScore, AppTheme.successGreen, sf), // Verde do tema
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf)), Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf, color: textColor)),
_scoreBox(opponentScore, Colors.grey, sf), _scoreBox(opponentScore, Colors.grey, sf),
], ],
), ),
const SizedBox(height: 8), SizedBox(height: 10 * sf),
TextButton.icon( TextButton.icon(
onPressed: () { onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))),
Navigator.push( icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: AppTheme.primaryRed),
context, label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: AppTheme.primaryRed, fontWeight: FontWeight.bold)),
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( style: TextButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), backgroundColor: AppTheme.primaryRed.withOpacity(0.1),
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf), padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)),
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
), ),
), ),
const SizedBox(height: 4), SizedBox(height: 6 * sf),
Text(status, style: const TextStyle(fontSize: 10, color: Colors.blue, fontWeight: FontWeight.bold)), Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)),
], ],
); );
} }
Widget _scoreBox(String pts, Color c) => Container( Widget _scoreBox(String pts, Color c, double sf) => Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)),
child: Text(pts, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
); );
} }
// --- POPUP DE CRIAÇÃO ---
class CreateGameDialogManual extends StatefulWidget {
final TeamController teamController;
final GameController gameController;
final double sf; // NOVA VARIÁVEL DE ESCALA
const CreateGameDialogManual({
super.key,
required this.teamController,
required this.gameController,
required this.sf,
});
@override
State<CreateGameDialogManual> createState() => _CreateGameDialogManualState();
}
class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
late TextEditingController _seasonController;
final TextEditingController _myTeamController = TextEditingController();
final TextEditingController _opponentController = TextEditingController();
bool _isLoading = false;
@override
void initState() {
super.initState();
_seasonController = TextEditingController(text: _calculateSeason());
}
String _calculateSeason() {
final now = DateTime.now();
return now.month >= 7 ? "${now.year}/${(now.year + 1).toString().substring(2)}" : "${now.year - 1}/${now.year.toString().substring(2)}";
}
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * widget.sf)),
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * widget.sf)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _seasonController,
style: TextStyle(fontSize: 14 * widget.sf),
decoration: InputDecoration(
labelText: 'Temporada',
labelStyle: TextStyle(fontSize: 14 * widget.sf),
border: const OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today, size: 20 * widget.sf)
),
),
SizedBox(height: 15 * widget.sf),
_buildSearch(label: "Minha Equipa", controller: _myTeamController, sf: widget.sf),
Padding(
padding: EdgeInsets.symmetric(vertical: 10 * widget.sf),
child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * widget.sf))
),
_buildSearch(label: "Adversário", controller: _opponentController, sf: widget.sf),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf))
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE74C3C),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * widget.sf)),
padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)
),
onPressed: _isLoading ? null : () async {
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
setState(() => _isLoading = true);
String? newGameId = await widget.gameController.createGame(
_myTeamController.text,
_opponentController.text,
_seasonController.text,
);
setState(() => _isLoading = false);
if (newGameId != null && context.mounted) {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PlacarPage(
gameId: newGameId,
myTeam: _myTeamController.text,
opponentTeam: _opponentController.text,
),
),
);
}
}
},
child: _isLoading
? SizedBox(width: 20 * widget.sf, height: 20 * widget.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * widget.sf)),
),
],
);
}
Widget _buildSearch({required String label, required TextEditingController controller, required double sf}) {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: widget.teamController.teamsStream,
builder: (context, snapshot) {
List<Map<String, dynamic>> teamList = snapshot.hasData ? snapshot.data! : [];
return Autocomplete<Map<String, dynamic>>(
displayStringForOption: (Map<String, dynamic> option) => option['name'].toString(),
optionsBuilder: (TextEditingValue val) {
if (val.text.isEmpty) return const Iterable<Map<String, dynamic>>.empty();
return teamList.where((t) =>
t['name'].toString().toLowerCase().contains(val.text.toLowerCase()));
},
onSelected: (Map<String, dynamic> selection) {
controller.text = selection['name'].toString();
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0,
borderRadius: BorderRadius.circular(8 * sf),
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 250 * sf, maxWidth: MediaQuery.of(context).size.width * 0.7),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final option = options.elementAt(index);
final String name = option['name'].toString();
final String? imageUrl = option['image_url'];
return ListTile(
leading: CircleAvatar(
radius: 20 * sf,
backgroundColor: Colors.grey.shade200,
backgroundImage: (imageUrl != null && imageUrl.isNotEmpty)
? NetworkImage(imageUrl)
: null,
child: (imageUrl == null || imageUrl.isEmpty)
? Icon(Icons.shield, color: Colors.grey, size: 20 * sf)
: null,
),
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * sf)),
onTap: () {
onSelected(option);
},
);
},
),
),
),
);
},
fieldViewBuilder: (ctx, txtCtrl, node, submit) {
if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) {
txtCtrl.text = controller.text;
}
txtCtrl.addListener(() {
controller.text = txtCtrl.text;
});
return TextField(
controller: txtCtrl,
focusNode: node,
style: TextStyle(fontSize: 14 * sf),
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(fontSize: 14 * sf),
prefixIcon: Icon(Icons.search, size: 20 * sf),
border: const OutlineInputBorder()
),
);
},
);
},
);
}
}

View File

@@ -25,7 +25,7 @@ class StatCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return SizedBox(
width: HomeConfig.cardwidthPadding, width: HomeConfig.cardwidthPadding,
height: HomeConfig.cardheightPadding, height: HomeConfig.cardheightPadding,
child: Card( child: Card(
@@ -33,7 +33,7 @@ class StatCard extends StatelessWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
side: isHighlighted side: isHighlighted
? BorderSide(color: Colors.amber, width: 2) ? const BorderSide(color: Colors.amber, width: 2)
: BorderSide.none, : BorderSide.none,
), ),
child: InkWell( child: InkWell(
@@ -68,7 +68,7 @@ class StatCard extends StatelessWidget {
title.toUpperCase(), title.toUpperCase(),
style: HomeConfig.titleStyle, style: HomeConfig.titleStyle,
), ),
SizedBox(height: 5), const SizedBox(height: 5),
Text( Text(
playerName, playerName,
style: HomeConfig.playerNameStyle, style: HomeConfig.playerNameStyle,
@@ -80,22 +80,16 @@ class StatCard extends StatelessWidget {
), ),
if (isHighlighted) if (isHighlighted)
Container( Container(
padding: EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.amber, color: Colors.amber,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: const Icon(Icons.star, size: 20, color: Colors.white),
Icons.star,
size: 20,
color: Colors.white,
),
), ),
], ],
), ),
const SizedBox(height: 10),
SizedBox(height: 10),
// Ícone // Ícone
Container( Container(
width: 60, width: 60,
@@ -104,51 +98,32 @@ class StatCard extends StatelessWidget {
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: Icon(icon, size: 30, color: Colors.white),
icon,
size: 30,
color: Colors.white,
),
), ),
const Spacer(),
Spacer(),
// Estatística // Estatística
Center( Center(
child: Column( child: Column(
children: [ children: [
Text( Text(statValue, style: HomeConfig.statValueStyle),
statValue, const SizedBox(height: 5),
style: HomeConfig.statValueStyle, Text(statLabel.toUpperCase(), style: HomeConfig.statLabelStyle),
),
SizedBox(height: 5),
Text(
statLabel.toUpperCase(),
style: HomeConfig.statLabelStyle,
),
], ],
), ),
), ),
const Spacer(),
Spacer(),
// Botão // Botão
Container( Container(
width: double.infinity, width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
child: Center( child: const Center(
child: Text( child: Text(
'VER DETALHES', 'VER DETALHES',
style: TextStyle( style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, letterSpacing: 1),
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
letterSpacing: 1,
),
), ),
), ),
), ),
@@ -174,11 +149,10 @@ class SportGrid extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (children.isEmpty) return SizedBox(); if (children.isEmpty) return const SizedBox();
return Column( return Column(
children: [ children: [
// Primeira linha
if (children.length >= 2) if (children.length >= 2)
Padding( Padding(
padding: EdgeInsets.only(bottom: spacing), padding: EdgeInsets.only(bottom: spacing),
@@ -191,8 +165,6 @@ class SportGrid extends StatelessWidget {
], ],
), ),
), ),
// Segunda linha
if (children.length >= 4) if (children.length >= 4)
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@@ -7,70 +7,104 @@ import 'package:playmaker/zone_map_dialog.dart';
// ============================================================================ // ============================================================================
class TopScoreboard extends StatelessWidget { class TopScoreboard extends StatelessWidget {
final PlacarController controller; final PlacarController controller;
const TopScoreboard({super.key, required this.controller}); final double sf;
const TopScoreboard({super.key, required this.controller, required this.sf});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 30), padding: EdgeInsets.symmetric(vertical: 10 * sf, horizontal: 35 * sf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF16202C), color: const Color(0xFF16202C),
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(15), bottomRight: Radius.circular(15)), borderRadius: BorderRadius.only(
border: Border.all(color: Colors.white, width: 2), bottomLeft: Radius.circular(22 * sf),
bottomRight: Radius.circular(22 * sf)
),
border: Border.all(color: Colors.white, width: 2.5 * sf),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false), _buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false, sf),
const SizedBox(width: 25), SizedBox(width: 30 * sf),
Column( Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6), padding: EdgeInsets.symmetric(horizontal: 18 * sf, vertical: 5 * sf),
decoration: BoxDecoration(color: const Color(0xFF2C3E50), borderRadius: BorderRadius.circular(6)), decoration: BoxDecoration(
child: Text(controller.formatTime(), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, fontFamily: 'monospace')), color: const Color(0xFF2C3E50),
borderRadius: BorderRadius.circular(9 * sf)
),
child: Text(
controller.formatTime(),
style: TextStyle(color: Colors.white, fontSize: 28 * sf, fontWeight: FontWeight.w900, fontFamily: 'monospace', letterSpacing: 2 * sf)
),
),
SizedBox(height: 5 * sf),
Text(
"PERÍODO ${controller.currentQuarter}",
style: TextStyle(color: Colors.orangeAccent, fontSize: 14 * sf, fontWeight: FontWeight.w900)
), ),
const SizedBox(height: 5),
Text("PERÍODO ${controller.currentQuarter}", style: const TextStyle(color: Colors.orangeAccent, fontSize: 14, fontWeight: FontWeight.bold)),
], ],
), ),
const SizedBox(width: 25), SizedBox(width: 30 * sf),
_buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true), _buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true, sf),
], ],
), ),
); );
} }
Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp) { Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp, double sf) {
int displayFouls = fouls > 5 ? 5 : fouls;
final timeoutIndicators = Row( final timeoutIndicators = Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: List.generate(3, (index) => Container( children: List.generate(3, (index) => Container(
margin: const EdgeInsets.symmetric(horizontal: 3), margin: EdgeInsets.symmetric(horizontal: 3.5 * sf),
width: 12, height: 12, width: 12 * sf, height: 12 * sf,
decoration: BoxDecoration(shape: BoxShape.circle, color: index < timeouts ? Colors.yellow : Colors.grey.shade600, border: Border.all(color: Colors.black26)), decoration: BoxDecoration(
shape: BoxShape.circle,
color: index < timeouts ? Colors.yellow : Colors.grey.shade600,
border: Border.all(color: Colors.white54, width: 1.5 * sf)
),
)), )),
); );
return Row(
crossAxisAlignment: CrossAxisAlignment.start, List<Widget> content = [
children: isOpp Column(
? [ children: [
Column(children: [_scoreBox(score, color), const SizedBox(height: 4), Text("FALTAS: $fouls", style: TextStyle(color: fouls >= 5 ? Colors.red : Colors.yellowAccent, fontSize: 12, fontWeight: FontWeight.bold)), timeoutIndicators]), _scoreBox(score, color, sf),
const SizedBox(width: 15), SizedBox(height: 7 * sf),
Text(name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)) timeoutIndicators
] ]
: [ ),
Text(name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), SizedBox(width: 18 * sf),
const SizedBox(width: 15), Column(
Column(children: [_scoreBox(score, color), const SizedBox(height: 4), Text("FALTAS: $fouls", style: TextStyle(color: fouls >= 5 ? Colors.red : Colors.yellowAccent, fontSize: 12, fontWeight: FontWeight.bold)), timeoutIndicators]) crossAxisAlignment: isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end,
] children: [
); Text(
name.toUpperCase(),
style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.2 * sf)
),
SizedBox(height: 5 * sf),
Text(
"FALTAS: $displayFouls",
style: TextStyle(color: displayFouls >= 5 ? Colors.redAccent : Colors.yellowAccent, fontSize: 13 * sf, fontWeight: FontWeight.bold)
),
],
)
];
return Row(crossAxisAlignment: CrossAxisAlignment.center, children: isOpp ? content : content.reversed.toList());
} }
Widget _scoreBox(int score, Color color) => Container( Widget _scoreBox(int score, Color color, double sf) => Container(
width: 50, height: 40, width: 58 * sf, height: 45 * sf,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(6)), decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(7 * sf)),
child: Text(score.toString(), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)), child: Text(score.toString(), style: TextStyle(color: Colors.white, fontSize: 26 * sf, fontWeight: FontWeight.w900)),
); );
} }
@@ -80,7 +114,9 @@ class TopScoreboard extends StatelessWidget {
class BenchPlayersList extends StatelessWidget { class BenchPlayersList extends StatelessWidget {
final PlacarController controller; final PlacarController controller;
final bool isOpponent; final bool isOpponent;
const BenchPlayersList({super.key, required this.controller, required this.isOpponent}); final double sf;
const BenchPlayersList({super.key, required this.controller, required this.isOpponent, required this.sf});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -96,24 +132,45 @@ class BenchPlayersList extends StatelessWidget {
final bool isFouledOut = fouls >= 5; final bool isFouledOut = fouls >= 5;
Widget avatarUI = Container( Widget avatarUI = Container(
margin: const EdgeInsets.only(bottom: 5), margin: EdgeInsets.only(bottom: 7 * sf),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.8 * sf),
boxShadow: [BoxShadow(color: Colors.black45, blurRadius: 5 * sf, offset: Offset(0, 2.5 * sf))]
),
child: CircleAvatar( child: CircleAvatar(
backgroundColor: isFouledOut ? Colors.grey.shade700 : teamColor, radius: 22 * sf,
child: Text(num, style: TextStyle(color: isFouledOut ? Colors.red.shade300 : Colors.white, fontSize: 14, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), backgroundColor: isFouledOut ? Colors.grey.shade800 : teamColor,
child: Text(
num,
style: TextStyle(
color: isFouledOut ? Colors.red.shade300 : Colors.white,
fontSize: 16 * sf,
fontWeight: FontWeight.bold,
decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none
)
),
), ),
); );
if (isFouledOut) { if (isFouledOut) {
return GestureDetector( return GestureDetector(
onTap: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $playerName não pode voltar (Expulso).'), backgroundColor: Colors.red)), onTap: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $playerName não pode voltar (Expulso).'), backgroundColor: Colors.red)),
child: avatarUI, child: avatarUI
); );
} }
return Draggable<String>( return Draggable<String>(
data: "$prefix$playerName", data: "$prefix$playerName",
feedback: Material(color: Colors.transparent, child: CircleAvatar(backgroundColor: teamColor, child: Text(num, style: const TextStyle(color: Colors.white)))), feedback: Material(
childWhenDragging: const Opacity(opacity: 0.5, child: SizedBox(width: 40, height: 40)), color: Colors.transparent,
child: CircleAvatar(
radius: 28 * sf,
backgroundColor: teamColor,
child: Text(num, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18 * sf))
)
),
childWhenDragging: Opacity(opacity: 0.5, child: SizedBox(width: 45 * sf, height: 45 * sf)),
child: avatarUI, child: avatarUI,
); );
}).toList(), }).toList(),
@@ -131,8 +188,9 @@ class PlayerCourtCard extends StatelessWidget {
final PlacarController controller; final PlacarController controller;
final String name; final String name;
final bool isOpponent; final bool isOpponent;
final double sf;
const PlayerCourtCard({super.key, required this.controller, required this.name, required this.isOpponent}); const PlayerCourtCard({super.key, required this.controller, required this.name, required this.isOpponent, required this.sf});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -146,12 +204,12 @@ class PlayerCourtCard extends StatelessWidget {
feedback: Material( feedback: Material(
color: Colors.transparent, color: Colors.transparent,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), padding: EdgeInsets.symmetric(horizontal: 18 * sf, vertical: 11 * sf),
decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(9 * sf)),
child: Text(name, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), child: Text(name, style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)),
), ),
), ),
childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, name, stats, teamColor, false, false)), childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, name, stats, teamColor, false, false, sf)),
child: DragTarget<String>( child: DragTarget<String>(
onAcceptWithDetails: (details) { onAcceptWithDetails: (details) {
final action = details.data; final action = details.data;
@@ -177,30 +235,27 @@ class PlayerCourtCard extends StatelessWidget {
// Se for 1 Ponto (Lance Livre), Falta, Ressalto ou Roubo, FAZ TUDO NORMAL! // Se for 1 Ponto (Lance Livre), Falta, Ressalto ou Roubo, FAZ TUDO NORMAL!
else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) { else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) {
controller.handleActionDrag(context, action, "$prefix$name"); controller.handleActionDrag(context, action, "$prefix$name");
} } else if (action.startsWith("bench_")) {
else if (action.startsWith("bench_")) {
controller.handleSubbing(context, action, name, isOpponent); controller.handleSubbing(context, action, name, isOpponent);
} }
}, },
builder: (context, candidateData, rejectedData) { builder: (context, candidateData, rejectedData) {
bool isSubbing = candidateData.any((data) => data != null && (data.startsWith("bench_my_") || data.startsWith("bench_opp_"))); 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_"))); bool isActionHover = candidateData.any((data) => data != null && (data.startsWith("add_") || data.startsWith("sub_") || data.startsWith("miss_")));
return _playerCardUI(number, name, stats, teamColor, isSubbing, isActionHover); return _playerCardUI(number, name, stats, teamColor, isSubbing, isActionHover, sf);
}, },
), ),
); );
} }
Widget _playerCardUI(String number, String name, Map<String, int> stats, Color teamColor, bool isSubbing, bool isActionHover, double sf) { Widget _playerCardUI(String number, String name, Map<String, int> stats, Color teamColor, bool isSubbing, bool isActionHover, double sf) {
// ... (Mantém o teu código de design _playerCardUI que já tinhas aqui dentro, fica igualzinho!)
bool isFouledOut = stats["fls"]! >= 5; bool isFouledOut = stats["fls"]! >= 5;
Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white; Color bgColor = isFouledOut ? Colors.red.shade50 : Colors.white;
Color borderColor = isFouledOut ? Colors.redAccent : Colors.transparent; Color borderColor = isFouledOut ? Colors.redAccent : Colors.transparent;
if (isSubbing) { if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = Colors.blue; }
bgColor = Colors.blue.shade50; borderColor = Colors.blue; else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = Colors.orange; }
} else if (isActionHover && !isFouledOut) {
bgColor = Colors.orange.shade50; borderColor = Colors.orange;
}
int fgm = stats["fgm"]!; int fgm = stats["fgm"]!;
int fga = stats["fga"]!; int fga = stats["fga"]!;
@@ -208,10 +263,11 @@ class PlayerCourtCard extends StatelessWidget {
String displayName = name.length > 12 ? "${name.substring(0, 10)}..." : name; String displayName = name.length > 12 ? "${name.substring(0, 10)}..." : name;
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: bgColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: borderColor, width: 2), color: bgColor,
boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 3))], borderRadius: BorderRadius.circular(11 * sf),
border: Border.all(color: borderColor, width: 1.8 * sf),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5 * sf, offset: Offset(2 * sf, 3.5 * sf))],
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(9 * sf), borderRadius: BorderRadius.circular(9 * sf),
@@ -232,19 +288,10 @@ class PlayerCourtCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(displayName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? Colors.red : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)),
displayName,
style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? Colors.red : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)
),
SizedBox(height: 2.5 * sf), SizedBox(height: 2.5 * sf),
Text( Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[700], fontWeight: FontWeight.w600)),
"${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", Text("${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls", style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[500], fontWeight: FontWeight.w600)),
style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[700], fontWeight: FontWeight.w600)
),
Text(
"${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls",
style: TextStyle(fontSize: 12 * sf, color: isFouledOut ? Colors.red : Colors.grey[500], fontWeight: FontWeight.w600)
),
], ],
), ),
), ),
@@ -261,12 +308,14 @@ class PlayerCourtCard extends StatelessWidget {
// ============================================================================ // ============================================================================
class ActionButtonsPanel extends StatelessWidget { class ActionButtonsPanel extends StatelessWidget {
final PlacarController controller; final PlacarController controller;
const ActionButtonsPanel({super.key, required this.controller}); final double sf;
const ActionButtonsPanel({super.key, required this.controller, required this.sf});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double baseSize = 65 * sf; // Reduzido (Antes era 75) final double baseSize = 65 * sf;
final double feedSize = 82 * sf; // Reduzido (Antes era 95) final double feedSize = 82 * sf;
final double gap = 7 * sf; final double gap = 7 * sf;
return Row( return Row(
@@ -274,116 +323,119 @@ class ActionButtonsPanel extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
_columnBtn([ _columnBtn([
_actionBtn("T.O", const Color(0xFF1E5BB2), () => controller.useTimeout(false), labelSize: 20), _dragAndTargetBtn("M1", Colors.redAccent, "miss_1", baseSize, feedSize, sf),
_dragAndTargetBtn("1", Colors.orange, "add_pts_1"), _dragAndTargetBtn("1", Colors.orange, "add_pts_1", baseSize, feedSize, sf),
_dragAndTargetBtn("1", Colors.orange, "sub_pts_1", isX: true), _dragAndTargetBtn("1", Colors.orange, "sub_pts_1", baseSize, feedSize, sf, isX: true),
_dragAndTargetBtn("STL", Colors.green, "add_stl"), _dragAndTargetBtn("STL", Colors.green, "add_stl", baseSize, feedSize, sf),
], gap),
]), SizedBox(width: gap * 1),
const SizedBox(width: 15),
_columnBtn([ _columnBtn([
_dragAndTargetBtn("M2", Colors.redAccent, "miss_2"), _dragAndTargetBtn("M2", Colors.redAccent, "miss_2", baseSize, feedSize, sf),
_dragAndTargetBtn("2", Colors.orange, "add_pts_2"), _dragAndTargetBtn("2", Colors.orange, "add_pts_2", baseSize, feedSize, sf),
_dragAndTargetBtn("2", Colors.orange, "sub_pts_2", isX: true), _dragAndTargetBtn("2", Colors.orange, "sub_pts_2", baseSize, feedSize, sf, isX: true),
_dragAndTargetBtn("AST", Colors.blueGrey, "add_ast"), _dragAndTargetBtn("AST", Colors.blueGrey, "add_ast", baseSize, feedSize, sf),
]), ], gap),
const SizedBox(width: 15), SizedBox(width: gap * 1),
_columnBtn([ _columnBtn([
_dragAndTargetBtn("M3", Colors.redAccent, "miss_3"), _dragAndTargetBtn("M3", Colors.redAccent, "miss_3", baseSize, feedSize, sf),
_dragAndTargetBtn("3", Colors.orange, "add_pts_3"), _dragAndTargetBtn("3", Colors.orange, "add_pts_3", baseSize, feedSize, sf),
_dragAndTargetBtn("3", Colors.orange, "sub_pts_3", isX: true), _dragAndTargetBtn("3", Colors.orange, "sub_pts_3", baseSize, feedSize, sf, isX: true),
_dragAndTargetBtn("TOV", Colors.redAccent, "add_tov"), _dragAndTargetBtn("TOV", Colors.redAccent, "add_tov", baseSize, feedSize, sf),
]), ], gap),
const SizedBox(width: 15), SizedBox(width: gap * 1),
_columnBtn([ _columnBtn([
_actionBtn("T.O", const Color(0xFFD92C2C), () => controller.useTimeout(true), labelSize: 20), _dragAndTargetBtn("ORB", const Color(0xFF1E2A38), "add_orb", baseSize, feedSize, sf, icon: Icons.sports_basketball),
_dragAndTargetBtn("ORB", const Color(0xFF1E2A38), "add_rbs", icon: Icons.sports_basketball), _dragAndTargetBtn("DRB", const Color(0xFF1E2A38), "add_drb", baseSize, feedSize, sf, icon: Icons.sports_basketball),
_dragAndTargetBtn("DRB", const Color(0xFF1E2A38), "add_rbs", icon: Icons.sports_basketball), _dragAndTargetBtn("BLK", Colors.deepPurple, "add_blk", baseSize, feedSize, sf, icon: Icons.front_hand),
], gap),
_dragAndTargetBtn("BLK", Colors.deepPurple, "add_blk", icon: Icons.front_hand),
]),
const SizedBox(width: 15),
_columnBtn([
])
], ],
); );
} }
// Mantenha os métodos _columnBtn, _dragAndTargetBtn, _actionBtn e _circle exatamente como estão Widget _columnBtn(List<Widget> children, double gap) {
Widget _columnBtn(List<Widget> children) => Column(mainAxisSize: MainAxisSize.min, children: children.map((c) => Padding(padding: const EdgeInsets.only(bottom: 8), child: c)).toList()); return Column(
mainAxisSize: MainAxisSize.min,
children: children.map((c) => Padding(padding: EdgeInsets.only(bottom: gap), child: c)).toList()
);
}
Widget _dragAndTargetBtn(String label, Color color, String actionData, {IconData? icon, bool isX = false}) { Widget _dragAndTargetBtn(String label, Color color, String actionData, double baseSize, double feedSize, double sf, {IconData? icon, bool isX = false}) {
return Draggable<String>( return Draggable<String>(
data: actionData, data: actionData,
feedback: _circle(label, color, icon, true, isX: isX), feedback: _circle(label, color, icon, true, baseSize, feedSize, sf, isX: isX),
childWhenDragging: Opacity(opacity: 0.5, child: _circle(label, color, icon, false, isX: isX)), childWhenDragging: Opacity(
opacity: 0.5,
child: _circle(label, color, icon, false, baseSize, feedSize, sf, isX: isX)
),
child: DragTarget<String>( child: DragTarget<String>(
onAcceptWithDetails: (details) {}, onAcceptWithDetails: (details) {}, // O PlayerCourtCard é que processa a ação!
builder: (context, candidateData, rejectedData) { builder: (context, candidateData, rejectedData) {
bool isHovered = candidateData.any((data) => data != null && data.startsWith("player_")); bool isHovered = candidateData.any((data) => data != null && data.startsWith("player_"));
return Transform.scale( return Transform.scale(
scale: isHovered ? 1.15 : 1.0, scale: isHovered ? 1.15 : 1.0,
child: Container(decoration: isHovered ? BoxDecoration(shape: BoxShape.circle, boxShadow: const [BoxShadow(color: Colors.white, blurRadius: 10, spreadRadius: 3)]) : null, child: _circle(label, color, icon, false, isX: isX)), child: Container(
decoration: isHovered ? BoxDecoration(shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.white, blurRadius: 10 * sf, spreadRadius: 3 * sf)]) : null,
child: _circle(label, color, icon, false, baseSize, feedSize, sf, isX: isX)
),
); );
} }
), ),
); );
} }
Widget _actionBtn(String label, Color color, VoidCallback onTap, {IconData? icon, bool isX = false, double labelSize = 24}) { Widget _circle(String label, Color color, IconData? icon, bool isFeed, double baseSize, double feedSize, double sf, {bool isX = false}) {
return GestureDetector(onTap: onTap, child: _circle(label, color, icon, false, fontSize: labelSize, isX: isX)); double size = isFeed ? feedSize : baseSize;
}
Widget _circle(String label, Color color, IconData? icon, bool isFeed, {double fontSize = 20, bool isX = false}) {
Widget content; Widget content;
bool isPointBtn = label == "1" || label == "2" || label == "3" || label == "M2" || label == "M3"; bool isPointBtn = label == "1" || label == "2" || label == "3" || label == "M1" || label == "M2" || label == "M3";
bool isBlkBtn = label == "BLK"; bool isBlkBtn = label == "BLK";
if (isPointBtn) { if (isPointBtn) {
content = Stack( content = Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
Container(width: isFeed ? 55 : 45, height: isFeed ? 55 : 45, decoration: const BoxDecoration(color: Colors.black, shape: BoxShape.circle)), 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: isFeed ? 65 : 55), Icon(Icons.sports_basketball, color: color, size: size * 0.9),
Stack( Stack(
children: [ children: [
Text(label, style: TextStyle(fontSize: isFeed ? 26 : 22, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = 3..color = Colors.white, decoration: TextDecoration.none)), Text(label, style: TextStyle(fontSize: 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: isFeed ? 26 : 22, fontWeight: FontWeight.w900, color: Colors.black, decoration: TextDecoration.none)), Text(label, style: TextStyle(fontSize: size * 0.38, fontWeight: FontWeight.w900, color: Colors.black, decoration: TextDecoration.none)),
], ],
), ),
], ],
); );
} } else if (isBlkBtn) {
else if (isBlkBtn) {
content = Stack( content = Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
Icon(Icons.front_hand, color: const Color.fromARGB(207, 56, 52, 52), size: isFeed ? 55 : 45), Icon(Icons.front_hand, color: const Color.fromARGB(207, 56, 52, 52), size: size * 0.75),
Stack( Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
Text(label, style: TextStyle(fontSize: isFeed ? 18 : 16, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = 3..color = Colors.black, decoration: TextDecoration.none)), Text(label, style: TextStyle(fontSize: 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: isFeed ? 18 : 16, fontWeight: FontWeight.w900, color: Colors.white, decoration: TextDecoration.none)), Text(label, style: TextStyle(fontSize: size * 0.28, fontWeight: FontWeight.w900, color: Colors.white, decoration: TextDecoration.none)),
], ],
), ),
], ],
); );
} else if (icon != null) { } else if (icon != null) {
content = Icon(icon, color: Colors.white, size: 30); content = Icon(icon, color: Colors.white, size: size * 0.5);
} else { } else {
content = Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: fontSize, decoration: TextDecoration.none)); content = Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: size * 0.35, decoration: TextDecoration.none));
} }
return Stack( return Stack(
clipBehavior: Clip.none, alignment: Alignment.bottomRight, clipBehavior: Clip.none,
alignment: Alignment.bottomRight,
children: [ children: [
Container( Container(
width: size, height: size, width: size, height: size,
decoration: (isPointBtn || isBlkBtn) ? const BoxDecoration(color: Colors.transparent) : BoxDecoration(gradient: RadialGradient(colors: [color.withOpacity(0.7), color], radius: 0.8), shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black38, blurRadius: 6 * sf, offset: Offset(0, 3 * sf))]), decoration: (isPointBtn || isBlkBtn)
? const BoxDecoration(color: Colors.transparent)
: BoxDecoration(gradient: RadialGradient(colors: [color.withOpacity(0.7), color], radius: 0.8), shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black38, blurRadius: 6 * sf, offset: Offset(0, 3 * sf))]),
alignment: Alignment.center, alignment: Alignment.center,
child: content, child: content,
), ),
if (isX) Positioned(top: 0, right: 0, child: Container(decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), child: Icon(Icons.cancel, color: Colors.red, size: isFeed ? 28 : 24))), if (isX) Positioned(top: 0, right: 0, child: Container(decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), child: Icon(Icons.cancel, color: Colors.red, size: size * 0.4))),
], ],
); );
} }

View File

@@ -7,154 +7,61 @@ import '../utils/size_extension.dart'; // 👇 O NOSSO SUPERPODER!
// --- CABEÇALHO --- // --- CABEÇALHO ---
class StatsHeader extends StatelessWidget { class StatsHeader extends StatelessWidget {
final Team team; final Team team;
final TeamController controller;
final VoidCallback onFavoriteTap;
final double sf; // <-- Variável de escala
const TeamCard({ const StatsHeader({super.key, required this.team});
super.key,
required this.team,
required this.controller,
required this.onFavoriteTap,
required this.sf,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Container(
color: Colors.white, padding: EdgeInsets.only(
elevation: 3, top: 50 * context.sf,
margin: EdgeInsets.only(bottom: 12 * sf), left: 20 * context.sf,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)), right: 20 * context.sf,
child: ListTile( bottom: 20 * context.sf
contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf), ),
decoration: BoxDecoration(
// --- 1. IMAGEM + FAVORITO --- color: AppTheme.primaryRed, // 👇 Usando a cor do teu tema!
leading: Stack( borderRadius: BorderRadius.only(
clipBehavior: Clip.none, bottomLeft: Radius.circular(30 * context.sf),
children: [ bottomRight: Radius.circular(30 * context.sf)
CircleAvatar(
radius: 28 * sf,
backgroundColor: Colors.grey[200],
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
? NetworkImage(team.imageUrl)
: null,
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
? Text(
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
style: TextStyle(fontSize: 24 * sf),
)
: null,
),
Positioned(
left: -15 * sf,
top: -10 * sf,
child: IconButton(
icon: Icon(
team.isFavorite ? Icons.star : Icons.star_border,
color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1),
size: 28 * sf,
shadows: [
Shadow(
color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1),
blurRadius: 4 * sf,
),
],
),
onPressed: onFavoriteTap,
),
),
],
),
// --- 2. TÍTULO ---
title: Text(
team.name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf),
overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos
),
// --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) ---
subtitle: Padding(
padding: EdgeInsets.only(top: 6.0 * sf),
child: Row(
children: [
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
SizedBox(width: 4 * sf),
// 👇 A CORREÇÃO ESTÁ AQUI: StreamBuilder em vez de FutureBuilder 👇
StreamBuilder<int>(
stream: controller.getPlayerCountStream(team.id),
initialData: 0,
builder: (context, snapshot) {
final count = snapshot.data ?? 0;
return Text(
"$count Jogs.", // Abreviado para poupar espaço
style: TextStyle(
color: count > 0 ? Colors.green[700] : Colors.orange,
fontWeight: FontWeight.bold,
fontSize: 13 * sf,
),
);
},
),
SizedBox(width: 8 * sf),
Expanded( // Garante que a temporada se adapta se faltar espaço
child: Text(
"| ${team.season}",
style: TextStyle(color: Colors.grey, fontSize: 13 * sf),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
// --- 4. BOTÕES (Estatísticas e Apagar) ---
trailing: Row(
mainAxisSize: MainAxisSize.min, // <-- ISTO RESOLVE O OVERFLOW DAS RISCAS AMARELAS
children: [
IconButton(
tooltip: 'Ver Estatísticas',
icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * sf),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TeamStatsPage(team: team),
),
);
},
),
IconButton(
tooltip: 'Eliminar Equipa',
icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * sf),
onPressed: () => _confirmDelete(context),
),
],
), ),
), ),
); child: Row(
} children: [
IconButton(
void _confirmDelete(BuildContext context) { icon: Icon(Icons.arrow_back, color: Colors.white, size: 24 * context.sf),
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold)),
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * sf)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf)),
), ),
TextButton( SizedBox(width: 10 * context.sf),
onPressed: () { CircleAvatar(
controller.deleteTeam(team.id); radius: 24 * context.sf,
Navigator.pop(context); backgroundColor: Colors.white24,
}, backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * sf)), ? NetworkImage(team.imageUrl)
: null,
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
? Text(
team.imageUrl.isEmpty ? "🛡️" : team.imageUrl,
style: TextStyle(fontSize: 20 * context.sf),
)
: null,
),
SizedBox(width: 15 * context.sf),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
team.name,
style: TextStyle(color: Colors.white, fontSize: 20 * context.sf, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
Text(
team.season,
style: TextStyle(color: Colors.white70, fontSize: 14 * context.sf)
),
],
),
), ),
], ],
), ),
@@ -162,90 +69,164 @@ class StatsHeader extends StatelessWidget {
} }
} }
// --- DIALOG DE CRIAÇÃO --- // --- CARD DE RESUMO ---
class CreateTeamDialog extends StatefulWidget { class StatsSummaryCard extends StatelessWidget {
final Function(String name, String season, String imageUrl) onConfirm; final int total;
final double sf; // Recebe a escala
const CreateTeamDialog({super.key, required this.onConfirm, required this.sf}); const StatsSummaryCard({super.key, required this.total});
@override
State<CreateTeamDialog> createState() => _CreateTeamDialogState();
}
class _CreateTeamDialogState extends State<CreateTeamDialog> {
final TextEditingController _nameController = TextEditingController();
final TextEditingController _imageController = TextEditingController();
String _selectedSeason = '2024/25';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( // 👇 Adaptável ao Modo Escuro
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)), final cardColor = Theme.of(context).brightness == Brightness.dark
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold)), ? const Color(0xFF1E1E1E)
content: SingleChildScrollView( : Colors.white;
child: Column(
mainAxisSize: MainAxisSize.min, return Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * context.sf)),
child: Container(
padding: EdgeInsets.all(20 * context.sf),
decoration: BoxDecoration(
color: cardColor,
borderRadius: BorderRadius.circular(20 * context.sf),
border: Border.all(color: Colors.grey.withOpacity(0.15)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
TextField( Row(
controller: _nameController, children: [
style: TextStyle(fontSize: 14 * widget.sf), Icon(Icons.groups, color: AppTheme.primaryRed, size: 28 * context.sf), // 👇 Cor do tema
decoration: InputDecoration( SizedBox(width: 10 * context.sf),
labelText: 'Nome da Equipa', Text(
labelStyle: TextStyle(fontSize: 14 * widget.sf) "Total de Membros",
), style: TextStyle(
textCapitalization: TextCapitalization.words, color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável
fontSize: 16 * context.sf,
fontWeight: FontWeight.w600
)
),
],
), ),
SizedBox(height: 15 * widget.sf), Text(
DropdownButtonFormField<String>( "$total",
value: _selectedSeason, style: TextStyle(
decoration: InputDecoration( color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável
labelText: 'Temporada', fontSize: 28 * context.sf,
labelStyle: TextStyle(fontSize: 14 * widget.sf) fontWeight: FontWeight.bold
), )
style: TextStyle(fontSize: 14 * widget.sf, color: Colors.black87),
items: ['2023/24', '2024/25', '2025/26']
.map((s) => DropdownMenuItem(value: s, child: Text(s)))
.toList(),
onChanged: (val) => setState(() => _selectedSeason = val!),
),
SizedBox(height: 15 * widget.sf),
TextField(
controller: _imageController,
style: TextStyle(fontSize: 14 * widget.sf),
decoration: InputDecoration(
labelText: 'URL Imagem ou Emoji',
labelStyle: TextStyle(fontSize: 14 * widget.sf),
hintText: 'Ex: 🏀 ou https://...',
hintStyle: TextStyle(fontSize: 14 * widget.sf)
),
), ),
], ],
), ),
), ),
actions: [ );
TextButton( }
onPressed: () => Navigator.pop(context), }
child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf))
), // --- TÍTULO DE SECÇÃO ---
ElevatedButton( class StatsSectionTitle extends StatelessWidget {
style: ElevatedButton.styleFrom( final String title;
backgroundColor: const Color(0xFFE74C3C),
padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf) const StatsSectionTitle({super.key, required this.title});
),
onPressed: () { @override
if (_nameController.text.trim().isNotEmpty) { Widget build(BuildContext context) {
widget.onConfirm( return Column(
_nameController.text.trim(), crossAxisAlignment: CrossAxisAlignment.start,
_selectedSeason, children: [
_imageController.text.trim(), Text(
); title,
Navigator.pop(context); style: TextStyle(
} fontSize: 18 * context.sf,
}, fontWeight: FontWeight.bold,
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)), color: Theme.of(context).colorScheme.onSurface // 👇 Adaptável
)
), ),
Divider(color: Colors.grey.withOpacity(0.3)),
], ],
); );
} }
} }
// --- CARD DA PESSOA (JOGADOR/TREINADOR) ---
class PersonCard extends StatelessWidget {
final Person person;
final bool isCoach;
final VoidCallback onEdit;
final VoidCallback onDelete;
const PersonCard({
super.key,
required this.person,
required this.isCoach,
required this.onEdit,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
// 👇 Cores adaptáveis para o Card
final defaultBg = Theme.of(context).brightness == Brightness.dark
? const Color(0xFF1E1E1E)
: Colors.white;
final coachBg = Theme.of(context).brightness == Brightness.dark
? AppTheme.warningAmber.withOpacity(0.1) // Amarelo escuro se for modo noturno
: const Color(0xFFFFF9C4); // Amarelo claro original
return Card(
margin: EdgeInsets.only(top: 12 * context.sf),
elevation: 2,
color: isCoach ? coachBg : defaultBg,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * context.sf)),
child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 4 * context.sf),
leading: isCoach
? CircleAvatar(
radius: 22 * context.sf,
backgroundColor: AppTheme.warningAmber, // 👇 Cor do tema
child: Icon(Icons.person, color: Colors.white, size: 24 * context.sf)
)
: Container(
width: 45 * context.sf,
height: 45 * context.sf,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppTheme.primaryRed.withOpacity(0.1), // 👇 Cor do tema
borderRadius: BorderRadius.circular(10 * context.sf)
),
child: Text(
person.number ?? "J",
style: TextStyle(
color: AppTheme.primaryRed, // 👇 Cor do tema
fontWeight: FontWeight.bold,
fontSize: 16 * context.sf
)
),
),
title: Text(
person.name,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16 * context.sf,
color: Theme.of(context).colorScheme.onSurface, // 👇 Adaptável
)
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.edit_outlined, color: Colors.blue, size: 22 * context.sf),
onPressed: onEdit,
),
IconButton(
icon: Icon(Icons.delete_outline, color: AppTheme.primaryRed, size: 22 * context.sf), // 👇 Cor do tema
onPressed: onDelete,
),
],
),
),
);
}
}