Compare commits

...

3 Commits

20 changed files with 1244 additions and 1171 deletions

View File

@@ -9,7 +9,7 @@ android {
namespace = "com.example.playmaker" namespace = "com.example.playmaker"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
//ndkVersion = flutter.ndkVersion //ndkVersion = flutter.ndkVersion
ndkVersion = "27.0.12077973" ndkVersion = "26.1.10909125"
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

View File

@@ -1,208 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:math' as math;
class CalibradorPage extends StatefulWidget {
const CalibradorPage({super.key});
@override
State<CalibradorPage> createState() => _CalibradorPageState();
}
class _CalibradorPageState extends State<CalibradorPage> {
// --- 👇 VALORES INICIAIS 👇 ---
double hoopBaseX = 0.08;
double arcRadius = 0.28;
double cornerY = 0.40;
// -----------------------------------------------------
@override
void initState() {
super.initState();
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeLeft,
]);
}
@override
void dispose() {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
super.dispose();
}
@override
Widget build(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
// O MESMO CÁLCULO EXATO DO PLACAR
final double sf = math.min(wScreen / 1150, hScreen / 720);
return Scaffold(
backgroundColor: const Color(0xFF266174),
body: SafeArea(
top: false,
bottom: false,
child: Stack(
children: [
// 👇 1. O CAMPO COM AS MARGENS EXATAS DO PLACAR 👇
Container(
margin: EdgeInsets.only(left: 65 * sf, right: 65 * sf, bottom: 55 * sf),
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2.5),
image: const DecorationImage(
image: AssetImage('assets/campo.png'),
fit: BoxFit.fill,
),
),
child: LayoutBuilder(
builder: (context, constraints) {
return CustomPaint(
painter: LinePainter(
hoopBaseX: hoopBaseX,
arcRadius: arcRadius,
cornerY: cornerY,
color: Colors.redAccent,
width: constraints.maxWidth,
height: constraints.maxHeight,
),
);
},
),
),
// 👇 2. TOPO: MOSTRADORES DE VALORES COM FITTEDBOX (Não transborda) 👇
Positioned(
top: 0, left: 0, right: 0,
child: Container(
color: Colors.black87.withOpacity(0.8),
padding: EdgeInsets.symmetric(vertical: 5 * sf, horizontal: 15 * sf),
child: FittedBox( // Isto impede o ecrã de dar o erro dos 179 pixels!
fit: BoxFit.scaleDown,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildValueDisplay("Aro X", hoopBaseX, sf),
SizedBox(width: 20 * sf),
_buildValueDisplay("Raio", arcRadius, sf),
SizedBox(width: 20 * sf),
_buildValueDisplay("Canto", cornerY, sf),
SizedBox(width: 30 * sf),
ElevatedButton.icon(
onPressed: () => Navigator.pop(context),
icon: Icon(Icons.check, size: 18 * sf),
label: Text("FECHAR", style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold)),
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
)
],
),
),
),
),
// 👇 3. FUNDO: SLIDERS (Com altura fixa para não dar o erro "hasSize") 👇
Positioned(
bottom: 0, left: 0, right: 0,
child: Container(
color: Colors.black87.withOpacity(0.8),
height: 80 * sf, // Altura segura para os sliders
child: Row(
children: [
Expanded(child: _buildSlider("Pos. do Aro", hoopBaseX, 0.0, 0.25, (val) => setState(() => hoopBaseX = val), sf)),
Expanded(child: _buildSlider("Tam. da Curva", arcRadius, 0.1, 0.5, (val) => setState(() => arcRadius = val), sf)),
Expanded(child: _buildSlider("Pos. do Canto", cornerY, 0.2, 0.5, (val) => setState(() => cornerY = val), sf)),
],
),
),
),
],
),
),
);
}
Widget _buildValueDisplay(String label, double value, double sf) {
return Row(
children: [
Text("$label: ", style: TextStyle(color: Colors.white70, fontSize: 16 * sf)),
Text(value.toStringAsFixed(3), style: TextStyle(color: Colors.yellow, fontSize: 20 * sf, fontWeight: FontWeight.bold)),
],
);
}
Widget _buildSlider(String label, double value, double min, double max, ValueChanged<double> onChanged, double sf) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(label, style: TextStyle(color: Colors.white, fontSize: 12 * sf)),
SizedBox(
height: 40 * sf, // Altura exata para o Slider não crashar
child: Slider(
value: value, min: min, max: max,
activeColor: Colors.yellow, inactiveColor: Colors.white24,
onChanged: onChanged,
),
),
],
);
}
}
// ==============================================================
// 📐 PINTOR: DESENHA A LINHA MATEMÁTICA NA TELA
// ==============================================================
class LinePainter extends CustomPainter {
final double hoopBaseX;
final double arcRadius;
final double cornerY;
final Color color;
final double width;
final double height;
LinePainter({
required this.hoopBaseX, required this.arcRadius, required this.cornerY,
required this.color, required this.width, required this.height,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 4;
double aspectRatio = width / height;
double hoopY = 0.50 * height;
// O cornerY controla a que distância do meio (50%) estão as linhas retas
double cornerDistY = cornerY * height;
// --- CESTO ESQUERDO ---
double hoopLX = hoopBaseX * width;
canvas.drawLine(Offset(0, hoopY - cornerDistY), Offset(width * 0.35, hoopY - cornerDistY), paint); // Cima
canvas.drawLine(Offset(0, hoopY + cornerDistY), Offset(width * 0.35, hoopY + cornerDistY), paint); // Baixo
canvas.drawArc(
Rect.fromCenter(center: Offset(hoopLX, hoopY), width: arcRadius * width * 2 / aspectRatio, height: arcRadius * height * 2),
-math.pi / 2, math.pi, false, paint,
);
// --- CESTO DIREITO ---
double hoopRX = (1.0 - hoopBaseX) * width;
canvas.drawLine(Offset(width, hoopY - cornerDistY), Offset(width * 0.65, hoopY - cornerDistY), paint); // Cima
canvas.drawLine(Offset(width, hoopY + cornerDistY), Offset(width * 0.65, hoopY + cornerDistY), paint); // Baixo
canvas.drawArc(
Rect.fromCenter(center: Offset(hoopRX, hoopY), width: arcRadius * width * 2 / aspectRatio, height: arcRadius * height * 2),
math.pi / 2, math.pi, false, paint,
);
}
@override
bool shouldRepaint(covariant LinePainter oldDelegate) {
return oldDelegate.hoopBaseX != hoopBaseX || oldDelegate.arcRadius != arcRadius || oldDelegate.cornerY != cornerY;
}
}

View File

@@ -4,34 +4,25 @@ import '../models/game_model.dart';
class GameController { class GameController {
final _supabase = Supabase.instance.client; final _supabase = Supabase.instance.client;
// 1. LER JOGOS (Com Filtros Opcionais) // 1. LER JOGOS (Stream em Tempo Real)
Stream<List<Game>> getFilteredGames({String? teamFilter, String? seasonFilter}) { Stream<List<Game>> get gamesStream {
return _supabase return _supabase
.from('games') .from('games') // 1. Fica à escuta da tabela original (Garante o Tempo Real!)
.stream(primaryKey: ['id']) .stream(primaryKey: ['id'])
.asyncMap((event) async { .asyncMap((event) async {
// 2. Sempre que a tabela 'games' mudar (novo jogo, alteração de resultado),
// vamos buscar os dados já misturados com as imagens à nossa View.
final viewData = await _supabase
.from('games_with_logos')
.select()
.order('game_date', ascending: false);
// 👇 A CORREÇÃO ESTÁ AQUI: Lê diretamente da tabela 'games' // 3. Convertemos para a nossa lista de objetos Game
var query = _supabase.from('games').select();
// Aplica o filtro de Temporada
if (seasonFilter != null && seasonFilter.isNotEmpty && seasonFilter != 'Todas') {
query = query.eq('season', seasonFilter);
}
// Aplica o filtro de Equipa (Procura em casa ou fora)
if (teamFilter != null && teamFilter.isNotEmpty && teamFilter != 'Todas') {
query = query.or('my_team.eq.$teamFilter,opponent_team.eq.$teamFilter');
}
// Executa a query com a ordenação por data
final viewData = await query.order('game_date', ascending: false);
return viewData.map((json) => Game.fromMap(json)).toList(); return viewData.map((json) => Game.fromMap(json)).toList();
}); });
} }
// 2. CRIAR JOGO // 2. CRIAR JOGO
// 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 {
try { try {
final response = await _supabase.from('games').insert({ final response = await _supabase.from('games').insert({
@@ -40,16 +31,18 @@ class GameController {
'season': season, 'season': season,
'my_score': 0, 'my_score': 0,
'opponent_score': 0, 'opponent_score': 0,
'status': 'Decorrer', 'status': 'Decorrer', // Começa como "Decorrer"
'game_date': DateTime.now().toIso8601String(), 'game_date': DateTime.now().toIso8601String(),
}).select().single(); }).select().single(); // .select().single() retorna o objeto criado
return response['id']; return response['id']; // Retorna o UUID gerado pelo Supabase
} catch (e) { } catch (e) {
print("Erro ao criar jogo: $e"); print("Erro ao criar jogo: $e");
return null; return null;
} }
} }
void dispose() {} void dispose() {
// Não é necessário fechar streams do Supabase manualmente aqui
}
} }

View File

@@ -0,0 +1,158 @@
/*import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../models/person_model.dart';
class StatsController {
final SupabaseClient _supabase = Supabase.instance.client;
// 1. LER
Stream<List<Person>> getMembers(String teamId) {
return _supabase
.from('members')
.stream(primaryKey: ['id'])
.eq('team_id', teamId)
.order('name', ascending: true)
.map((data) => data.map((json) => Person.fromMap(json)).toList());
}
// 2. APAGAR
Future<void> deletePerson(String personId) async {
try {
await _supabase.from('members').delete().eq('id', personId);
} catch (e) {
debugPrint("Erro ao eliminar: $e");
}
}
// 3. DIÁLOGOS
void showAddPersonDialog(BuildContext context, String teamId) {
_showForm(context, teamId: teamId);
}
void showEditPersonDialog(BuildContext context, String teamId, Person person) {
_showForm(context, teamId: teamId, person: person);
}
// --- O POPUP ESTÁ AQUI ---
void _showForm(BuildContext context, {required String teamId, Person? person}) {
final isEdit = person != null;
final nameCtrl = TextEditingController(text: person?.name ?? '');
final numCtrl = TextEditingController(text: person?.number ?? '');
// Define o valor inicial
String selectedType = person?.type ?? 'Jogador';
showDialog(
context: context,
builder: (ctx) => StatefulBuilder(
builder: (ctx, setState) => AlertDialog(
title: Text(isEdit ? "Editar" : "Adicionar"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
// NOME
TextField(
controller: nameCtrl,
decoration: const InputDecoration(labelText: "Nome"),
textCapitalization: TextCapitalization.sentences,
),
const SizedBox(height: 10),
// FUNÇÃO
DropdownButtonFormField<String>(
value: selectedType,
decoration: const InputDecoration(labelText: "Função"),
items: ["Jogador", "Treinador"]
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
.toList(),
onChanged: (v) {
if (v != null) setState(() => selectedType = v);
},
),
// NÚMERO (Só aparece se for Jogador)
if (selectedType == "Jogador") ...[
const SizedBox(height: 10),
TextField(
controller: numCtrl,
decoration: const InputDecoration(labelText: "Número da Camisola"),
keyboardType: TextInputType.text, // Aceita texto para evitar erros
),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text("Cancelar")
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF00C853)),
onPressed: () async {
print("--- 1. CLICOU EM GUARDAR ---");
// Validação Simples
if (nameCtrl.text.trim().isEmpty) {
print("ERRO: Nome vazio");
return;
}
// Lógica do Número:
// Se for Treinador -> envia NULL
// Se for Jogador e estiver vazio -> envia NULL
// Se tiver texto -> envia o Texto
String? numeroFinal;
if (selectedType == "Treinador") {
numeroFinal = null;
} else {
numeroFinal = numCtrl.text.trim().isEmpty ? null : numCtrl.text.trim();
}
print("--- 2. DADOS A ENVIAR ---");
print("Nome: ${nameCtrl.text}");
print("Tipo: $selectedType");
print("Número: $numeroFinal");
try {
if (isEdit) {
await _supabase.from('members').update({
'name': nameCtrl.text.trim(),
'type': selectedType,
'number': numeroFinal,
}).eq('id', person!.id);
} else {
await _supabase.from('members').insert({
'team_id': teamId, // Verifica se este teamId é válido!
'name': nameCtrl.text.trim(),
'type': selectedType,
'number': numeroFinal,
});
}
print("--- 3. SUCESSO! FECHANDO DIÁLOGO ---");
if (ctx.mounted) Navigator.pop(ctx);
} catch (e) {
print("--- X. ERRO AO GUARDAR ---");
print(e.toString());
// MOSTRA O ERRO NO TELEMÓVEL
if (ctx.mounted) {
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(
content: Text("Erro: $e"),
backgroundColor: Colors.red,
duration: const Duration(seconds: 4),
),
);
}
}
},
child: const Text("Guardar", style: TextStyle(color: Colors.white)),
)
],
),
),
);
}
}*/

View File

@@ -1,24 +1,21 @@
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
class TeamController { class TeamController {
// Instância do cliente Supabase
final _supabase = Supabase.instance.client; final _supabase = Supabase.instance.client;
// 1. Variável fixa para guardar o Stream principal // 1. STREAM (Realtime)
late final Stream<List<Map<String, dynamic>>> teamsStream; // Adicionei o .map() no final para garantir que o Dart entende que é uma List<Map>
Stream<List<Map<String, dynamic>>> get teamsStream {
// 2. Dicionário (Cache) para não recriar Streams de contagem repetidos return _supabase
final Map<String, Stream<int>> _playerCountStreams = {};
TeamController() {
// INICIALIZAÇÃO: O stream é criado APENAS UMA VEZ quando abres a página!
teamsStream = _supabase
.from('teams') .from('teams')
.stream(primaryKey: ['id']) .stream(primaryKey: ['id'])
.order('name', ascending: true) .order('name', ascending: true)
.map((data) => List<Map<String, dynamic>>.from(data)); .map((data) => List<Map<String, dynamic>>.from(data));
} }
// 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({
@@ -33,50 +30,42 @@ class TeamController {
} }
} }
// ELIMINAR // 3. ELIMINAR
Future<void> deleteTeam(String id) async { Future<void> deleteTeam(String id) async {
try { try {
await _supabase.from('teams').delete().eq('id', id); await _supabase.from('teams').delete().eq('id', id);
// Limpa o cache deste teamId se a equipa for apagada
_playerCountStreams.remove(id);
} catch (e) { } catch (e) {
print("❌ Erro ao eliminar: $e"); print("❌ Erro ao eliminar: $e");
} }
} }
// FAVORITAR // 4. FAVORITAR
Future<void> toggleFavorite(String teamId, bool currentStatus) async { Future<void> toggleFavorite(String teamId, bool currentStatus) async {
try { try {
await _supabase await _supabase
.from('teams') .from('teams')
.update({'is_favorite': !currentStatus}) .update({'is_favorite': !currentStatus}) // Inverte o valor
.eq('id', teamId); .eq('id', teamId);
} catch (e) { } catch (e) {
print("❌ Erro ao favoritar: $e"); print("❌ Erro ao favoritar: $e");
} }
} }
// CONTAR JOGADORES (AGORA COM CACHE DE MEMÓRIA!) // 5. CONTAR JOGADORES
Stream<int> getPlayerCountStream(String teamId) { // CORRIGIDO: A sintaxe antiga dava erro. O método .count() é o correto agora.
// Se já criámos um "Tubo de ligação" para esta equipa, REUTILIZA-O! Future<int> getPlayerCount(String teamId) async {
if (_playerCountStreams.containsKey(teamId)) { try {
return _playerCountStreams[teamId]!; final count = await _supabase
.from('members')
.count() // Retorna diretamente o número inteiro
.eq('team_id', teamId);
return count;
} catch (e) {
print("Erro ao contar jogadores: $e");
return 0;
} }
// Se é a primeira vez que pede esta equipa, cria a ligação e guarda na memória
final newStream = _supabase
.from('members')
.stream(primaryKey: ['id'])
.eq('team_id', teamId)
.map((data) => data.length);
_playerCountStreams[teamId] = newStream; // Guarda no dicionário
return newStream;
} }
// LIMPEZA FINAL QUANDO SAÍMOS DA PÁGINA // Mantemos o dispose vazio para não quebrar a chamada na TeamsPage
void dispose() { void dispose() {}
// Limpamos o dicionário de streams para libertar memória RAM
_playerCountStreams.clear();
}
} }

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../dados_grafico.dart'; import '../dados_grafico.dart'; // Ajusta o caminho se der erro de import
class PieChartController extends ChangeNotifier { 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, draws: draws ?? _chartData.draws, // 👇 AGORA ELE ACEITA OS EMPATES
); );
notifyListeners(); notifyListeners();
} }

View File

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

View File

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

View File

@@ -5,23 +5,26 @@ class PieChartWidget extends StatelessWidget {
final double victoryPercentage; final double victoryPercentage;
final double defeatPercentage; final double defeatPercentage;
final double drawPercentage; final double drawPercentage;
final double sf; final double size;
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,
required this.sf, this.size = 140, // Aumentado para 400x300
}); });
@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(
@@ -29,7 +32,7 @@ class PieChartWidget extends StatelessWidget {
width: size, width: size,
height: size, height: size,
child: CustomPaint( child: CustomPaint(
painter: _DonutChartPainter( painter: _PieChartPainter(
victoryPercentage: victoryPercentage, victoryPercentage: victoryPercentage,
defeatPercentage: defeatPercentage, defeatPercentage: defeatPercentage,
drawPercentage: drawPercentage, drawPercentage: drawPercentage,
@@ -45,27 +48,24 @@ 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(
// 👇 Casa decimal aplicada aqui! '${(victoryPercentage * 100).toStringAsFixed(1)}%',
hasGames ? '${(victoryPercentage * 100).toStringAsFixed(1)}%' : '---',
style: TextStyle( style: TextStyle(
fontSize: size * (hasGames ? 0.20 : 0.15), fontSize: size * 0.18, // O texto cresce ou encolhe com o círculo
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: hasGames ? Colors.white : Colors.white54, color: Colors.white,
), ),
), ),
SizedBox(height: size * 0.02), SizedBox(height: size * 0.02),
Text( Text(
hasGames ? 'Vitórias' : 'Sem Jogos', 'Vitórias',
style: TextStyle( style: TextStyle(
fontSize: size * 0.08, fontSize: size * 0.10,
color: hasGames ? Colors.white70 : Colors.white38, color: Colors.white.withOpacity(0.8),
), ),
), ),
], ],
@@ -87,40 +87,59 @@ 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);
final radius = (size.width / 2) - (size.width * 0.1); // Margem de 5% para a linha de fora não ser cortada
final strokeWidth = size.width * 0.2; final radius = (size.width / 2) - (size.width * 0.05);
if (victoryPercentage == 0 && defeatPercentage == 0 && drawPercentage == 0) {
final bgPaint = Paint()
..color = Colors.white.withOpacity(0.05)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth;
canvas.drawCircle(center, radius, bgPaint);
return;
}
const victoryColor = Colors.green; const victoryColor = Colors.green;
const defeatColor = Colors.red; const defeatColor = Colors.red;
const drawColor = Colors.amber; const drawColor = Colors.yellow;
const borderColor = Colors.white30;
double startAngle = -math.pi / 2; double startAngle = -math.pi / 2;
void drawDonutSector(double percentage, Color color) { if (victoryPercentage > 0) {
if (percentage <= 0) return; final sweepAngle = 2 * math.pi * victoryPercentage;
final sweepAngle = 2 * math.pi * percentage; _drawSector(canvas, center, radius, startAngle, sweepAngle, victoryColor, size.width);
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.butt;
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, paint);
startAngle += sweepAngle; startAngle += sweepAngle;
} }
drawDonutSector(victoryPercentage, victoryColor); if (drawPercentage > 0) {
drawDonutSector(drawPercentage, drawColor); final sweepAngle = 2 * math.pi * drawPercentage;
drawDonutSector(defeatPercentage, defeatColor); _drawSector(canvas, center, radius, startAngle, sweepAngle, drawColor, size.width);
startAngle += sweepAngle;
}
if (defeatPercentage > 0) {
final sweepAngle = 2 * math.pi * defeatPercentage;
_drawSector(canvas, center, radius, startAngle, sweepAngle, defeatColor, size.width);
}
final borderPaint = Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = size.width * 0.02;
canvas.drawCircle(center, radius, borderPaint);
}
void _drawSector(Canvas canvas, Offset center, double radius, double startAngle, double sweepAngle, Color color, double totalWidth) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, true, paint);
if (sweepAngle < 2 * math.pi) {
final linePaint = Paint()
..color = Colors.white.withOpacity(0.5)
..style = PaintingStyle.stroke
..strokeWidth = totalWidth * 0.015;
final lineX = center.dx + radius * math.cos(startAngle);
final lineY = center.dy + radius * math.sin(startAngle);
canvas.drawLine(center, Offset(lineX, lineY), linePaint);
}
} }
@override @override

View File

@@ -2,6 +2,8 @@ class Game {
final String id; final String id;
final String myTeam; final String myTeam;
final String opponentTeam; final String opponentTeam;
final String? myTeamLogo; // URL da imagem
final String? opponentTeamLogo; // URL da imagem
final String myScore; final String myScore;
final String opponentScore; final String opponentScore;
final String status; final String status;
@@ -11,22 +13,26 @@ class Game {
required this.id, required this.id,
required this.myTeam, required this.myTeam,
required this.opponentTeam, required this.opponentTeam,
this.myTeamLogo,
this.opponentTeamLogo,
required this.myScore, required this.myScore,
required this.opponentScore, required this.opponentScore,
required this.status, required this.status,
required this.season, required this.season,
}); });
// No seu factory, certifique-se de mapear os campos da tabela (ou de um JOIN)
factory Game.fromMap(Map<String, dynamic> map) { factory Game.fromMap(Map<String, dynamic> map) {
return Game( return Game(
// O "?." converte para texto com segurança, e o "?? '...'" diz o que mostrar se for nulo (vazio) id: map['id'],
id: map['id']?.toString() ?? '', myTeam: map['my_team_name'],
myTeam: map['my_team']?.toString() ?? 'Desconhecida', opponentTeam: map['opponent_team_name'],
opponentTeam: map['opponent_team']?.toString() ?? 'Adversário', myTeamLogo: map['my_team_logo'], // Certifique-se que o Supabase retorna isto
myScore: map['my_score']?.toString() ?? '0', opponentTeamLogo: map['opponent_team_logo'],
opponentScore: map['opponent_score']?.toString() ?? '0', myScore: map['my_score'].toString(),
status: map['status']?.toString() ?? 'Terminado', opponentScore: map['opponent_score'].toString(),
season: map['season']?.toString() ?? 'Sem Época', status: map['status'],
season: map['season'],
); );
} }
} }

View File

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

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

View File

@@ -0,0 +1 @@
import 'package:flutter/material.dart';

View File

@@ -1,15 +1 @@
import 'package:flutter/material.dart'; // TODO Implement this library.
import 'dart:math' as math;
// Esta extensão adiciona o superpoder "sf" ao BuildContext
extension SizeExtension on BuildContext {
double get sf {
final double wScreen = MediaQuery.of(this).size.width;
final double hScreen = MediaQuery.of(this).size.height;
// Calcula e devolve a escala na hora!
return math.min(wScreen, hScreen) / 400;
}
}

View File

@@ -1,18 +1,27 @@
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, myTeam, opponentTeam, myScore, opponentScore, status, season; final String gameId;
final String? myTeamLogo, opponentTeamLogo; final String myTeam, opponentTeam, myScore, opponentScore, status, season;
final double sf; final String? myTeamLogo;
final String? opponentTeamLogo;
final double sf; // NOVA VARIÁVEL DE ESCALA
const GameResultCard({ const GameResultCard({
super.key, required this.gameId, required this.myTeam, required this.opponentTeam, super.key,
required this.myScore, required this.opponentScore, required this.status, required this.season, required this.gameId,
this.myTeamLogo, this.opponentTeamLogo, required this.sf, required this.myTeam,
required this.opponentTeam,
required this.myScore,
required this.opponentScore,
required this.status,
required this.season,
this.myTeamLogo,
this.opponentTeamLogo,
required this.sf, // OBRIGATÓRIO RECEBER A ESCALA
}); });
@override @override
@@ -22,76 +31,291 @@ class GameResultCard extends StatelessWidget {
final textColor = Theme.of(context).colorScheme.onSurface; final textColor = Theme.of(context).colorScheme.onSurface;
return Container( return Container(
margin: EdgeInsets.only(bottom: 16 * sf), margin: const EdgeInsets.only(bottom: 16),
padding: EdgeInsets.all(16 * sf), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: bgColor, // Usa a cor do tema color: Colors.white,
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, AppTheme.primaryRed, myTeamLogo, sf, textColor)), // Usa o primaryRed Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, sf)),
_buildScoreCenter(context, gameId, sf), _buildScoreCenter(context, gameId, sf),
Expanded(child: _buildTeamInfo(opponentTeam, textColor, opponentTeamLogo, sf, textColor)), Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, sf)),
], ],
), ),
); );
} }
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) { Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf) {
return Column( return Column(
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 24 * sf, radius: 24 * sf, // Ajuste do tamanho do logo
backgroundColor: color, backgroundColor: color,
backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, backgroundImage: (logoUrl != null && logoUrl.isNotEmpty)
child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * sf) : null, ? NetworkImage(logoUrl)
: null,
child: (logoUrl == null || logoUrl.isEmpty)
? Icon(Icons.shield, color: Colors.white, size: 24 * sf)
: null,
), ),
SizedBox(height: 6 * sf), const SizedBox(height: 4),
Text(name, Text(name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf, color: textColor), // Adapta à noite/dia style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf),
textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2, textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2, // Permite 2 linhas para nomes compridos não cortarem
), ),
], ],
); );
} }
Widget _buildScoreCenter(BuildContext context, String id, double sf) { 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, AppTheme.successGreen, sf), // Verde do tema _scoreBox(myScore, Colors.green, sf),
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf, color: textColor)), Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf)),
_scoreBox(opponentScore, Colors.grey, sf), _scoreBox(opponentScore, Colors.grey, sf),
], ],
), ),
SizedBox(height: 10 * sf), const SizedBox(height: 8),
TextButton.icon( TextButton.icon(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))), onPressed: () {
icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: AppTheme.primaryRed), Navigator.push(
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: AppTheme.primaryRed, fontWeight: FontWeight.bold)), context,
MaterialPageRoute(
builder: (context) => PlacarPage(
gameId: id,
myTeam: myTeam,
opponentTeam: opponentTeam,
),
),
);
},
icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: const Color(0xFFE74C3C)),
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: AppTheme.primaryRed.withOpacity(0.1), backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1),
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf), 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,
), ),
), ),
SizedBox(height: 6 * sf), const SizedBox(height: 4),
Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)), Text(status, style: const TextStyle(fontSize: 10, color: Colors.blue, fontWeight: FontWeight.bold)),
], ],
); );
} }
Widget _scoreBox(String pts, Color c, double sf) => Container( Widget _scoreBox(String pts, Color c) => Container(
padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)), decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8)),
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)), child: Text(pts, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
); );
} }
// --- POPUP DE CRIAÇÃO ---
class CreateGameDialogManual extends StatefulWidget {
final TeamController teamController;
final GameController gameController;
final double sf; // NOVA VARIÁVEL DE ESCALA
const CreateGameDialogManual({
super.key,
required this.teamController,
required this.gameController,
required this.sf,
});
@override
State<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

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/classe/home.config.dart';
class StatCard extends StatelessWidget { class StatCard extends StatelessWidget {
final String title; final String title;
@@ -10,11 +11,6 @@ class StatCard extends StatelessWidget {
final bool isHighlighted; final bool isHighlighted;
final VoidCallback? onTap; final VoidCallback? onTap;
// Variáveis novas para que o tamanho não fique preso à HomeConfig
final double sf;
final double cardWidth;
final double cardHeight;
const StatCard({ const StatCard({
super.key, super.key,
required this.title, required this.title,
@@ -25,30 +21,27 @@ class StatCard extends StatelessWidget {
required this.icon, required this.icon,
this.isHighlighted = false, this.isHighlighted = false,
this.onTap, this.onTap,
this.sf = 1.0, // Default 1.0 para não dar erro se não passares o valor
required this.cardWidth,
required this.cardHeight,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return Container(
width: cardWidth, width: HomeConfig.cardwidthPadding,
height: cardHeight, height: HomeConfig.cardheightPadding,
child: Card( child: Card(
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20 * sf), borderRadius: BorderRadius.circular(20),
side: isHighlighted side: isHighlighted
? BorderSide(color: Colors.amber, width: 2 * sf) ? BorderSide(color: Colors.amber, width: 2)
: BorderSide.none, : BorderSide.none,
), ),
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(20 * sf), borderRadius: BorderRadius.circular(20),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20 * sf), borderRadius: BorderRadius.circular(20),
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
@@ -59,14 +52,13 @@ class StatCard extends StatelessWidget {
), ),
), ),
child: Padding( child: Padding(
padding: EdgeInsets.all(16.0 * sf), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Cabeçalho // Cabeçalho
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: Column( child: Column(
@@ -74,12 +66,12 @@ class StatCard extends StatelessWidget {
children: [ children: [
Text( Text(
title.toUpperCase(), title.toUpperCase(),
style: TextStyle(fontSize: 11 * sf, fontWeight: FontWeight.bold, color: Colors.white70), style: HomeConfig.titleStyle,
), ),
SizedBox(height: 2 * sf), SizedBox(height: 5),
Text( Text(
playerName, playerName,
style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold, color: Colors.white), style: HomeConfig.playerNameStyle,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -88,38 +80,38 @@ class StatCard extends StatelessWidget {
), ),
if (isHighlighted) if (isHighlighted)
Container( Container(
padding: EdgeInsets.all(6 * sf), padding: EdgeInsets.all(8),
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Colors.amber, color: Colors.amber,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: Icon(
Icons.star, Icons.star,
size: 16 * sf, size: 20,
color: Colors.white, color: Colors.white,
), ),
), ),
], ],
), ),
SizedBox(height: 8 * sf), SizedBox(height: 10),
// Ícone // Ícone
Container( Container(
width: 45 * sf, width: 60,
height: 45 * sf, height: 60,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: Icon(
icon, icon,
size: 24 * sf, size: 30,
color: Colors.white, color: Colors.white,
), ),
), ),
const Spacer(), Spacer(),
// Estatística // Estatística
Center( Center(
@@ -127,26 +119,26 @@ class StatCard extends StatelessWidget {
children: [ children: [
Text( Text(
statValue, statValue,
style: TextStyle(fontSize: 34 * sf, fontWeight: FontWeight.bold, color: Colors.white), style: HomeConfig.statValueStyle,
), ),
SizedBox(height: 2 * sf), SizedBox(height: 5),
Text( Text(
statLabel.toUpperCase(), statLabel.toUpperCase(),
style: TextStyle(fontSize: 12 * sf, color: Colors.white70), style: HomeConfig.statLabelStyle,
), ),
], ],
), ),
), ),
const Spacer(), Spacer(),
// Botão // Botão
Container( Container(
width: double.infinity, width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 8 * sf), padding: EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(10 * sf), borderRadius: BorderRadius.circular(15),
), ),
child: Center( child: Center(
child: Text( child: Text(
@@ -154,7 +146,7 @@ class StatCard extends StatelessWidget {
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 11 * sf, fontSize: 14,
letterSpacing: 1, letterSpacing: 1,
), ),
), ),
@@ -177,12 +169,12 @@ class SportGrid extends StatelessWidget {
const SportGrid({ const SportGrid({
super.key, super.key,
required this.children, required this.children,
this.spacing = 20.0, // Valor padrão se não for passado nada this.spacing = HomeConfig.cardSpacing,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (children.isEmpty) return const SizedBox(); if (children.isEmpty) return SizedBox();
return Column( return Column(
children: [ children: [

View File

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

View File

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

View File

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