This commit is contained in:
2026-03-22 16:16:08 +00:00
parent be103c66b0
commit 9cf7915d12
6 changed files with 303 additions and 163 deletions

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:cached_network_image/cached_network_image.dart'; // 👇 A MAGIA DO CACHE AQUI
import 'package:playmaker/screens/team_stats_page.dart';
import 'package:playmaker/classe/theme.dart';
import '../controllers/team_controller.dart';
@@ -172,7 +173,6 @@ class _TeamsPageState extends State<TeamsPage> {
);
}
// 👇 AGORA USA FUTUREBUILDER E É MUITO MAIS RÁPIDO 👇
Widget _buildTeamsList() {
return FutureBuilder<List<Map<String, dynamic>>>(
future: controller.getTeamsWithStats(),
@@ -196,7 +196,7 @@ class _TeamsPageState extends State<TeamsPage> {
return RefreshIndicator(
color: AppTheme.primaryRed,
onRefresh: () async => setState(() {}), // Puxa para baixo para recarregar
onRefresh: () async => setState(() {}),
child: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf),
itemCount: data.length,
@@ -209,9 +209,9 @@ class _TeamsPageState extends State<TeamsPage> {
controller: controller,
onFavoriteTap: () async {
await controller.toggleFavorite(team.id, team.isFavorite);
setState(() {}); // Atualiza a estrela na hora
setState(() {});
},
onDelete: () => setState(() {}), // Atualiza a lista quando apaga
onDelete: () => setState(() {}),
sf: context.sf,
),
);
@@ -229,7 +229,7 @@ class _TeamsPageState extends State<TeamsPage> {
sf: context.sf,
onConfirm: (name, season, imageFile) async {
await controller.createTeam(name, season, imageFile);
setState(() {}); // 👇 Atualiza a lista quando acaba de criar a equipa!
setState(() {});
}
),
);
@@ -241,7 +241,7 @@ class TeamCard extends StatelessWidget {
final Team team;
final TeamController controller;
final VoidCallback onFavoriteTap;
final VoidCallback onDelete; // 👇 Avisa o pai quando é apagado
final VoidCallback onDelete;
final double sf;
const TeamCard({
@@ -257,6 +257,7 @@ class TeamCard extends StatelessWidget {
Widget build(BuildContext context) {
final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
final textColor = Theme.of(context).colorScheme.onSurface;
final double avatarSize = 56 * sf; // 2 * radius (28)
return Container(
margin: EdgeInsets.only(bottom: 12 * sf),
@@ -274,18 +275,29 @@ class TeamCard extends StatelessWidget {
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,
// 👇 AVATAR DA EQUIPA OTIMIZADO COM CACHE 👇
ClipOval(
child: Container(
width: avatarSize,
height: avatarSize,
color: Colors.grey.withOpacity(0.2),
child: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
? CachedNetworkImage(
imageUrl: team.imageUrl,
fit: BoxFit.cover,
fadeInDuration: Duration.zero, // Fica instantâneo no scroll
placeholder: (context, url) => const SizedBox(), // Fica só o fundo cinza
errorWidget: (context, url, error) => Center(
child: Text("🏀", style: TextStyle(fontSize: 24 * sf)),
),
)
: Center(
child: Text(
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
style: TextStyle(fontSize: 24 * sf),
),
),
),
),
Positioned(
left: -15 * sf,
@@ -313,8 +325,6 @@ class TeamCard extends StatelessWidget {
children: [
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
SizedBox(width: 4 * sf),
// 👇 ESTATÍSTICA MUITO MAIS LEVE. LÊ O VALOR DIRETAMENTE! 👇
Text(
"${team.playerCount} Jogs.",
style: TextStyle(
@@ -323,7 +333,6 @@ class TeamCard extends StatelessWidget {
fontSize: 13 * sf,
),
),
SizedBox(width: 8 * sf),
Expanded(
child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * sf), overflow: TextOverflow.ellipsis),
@@ -337,7 +346,7 @@ class TeamCard extends StatelessWidget {
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))).then((_) => onDelete()), // Atualiza se algo mudou
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))).then((_) => onDelete()),
),
IconButton(
tooltip: 'Eliminar Equipa',
@@ -351,7 +360,7 @@ class TeamCard extends StatelessWidget {
);
}
void _confirmDelete(BuildContext context, double sf, Color cardColor, Color textColor) {
void _confirmDelete(BuildContext context, double sf, Color cardColor, Color textColor) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
@@ -366,12 +375,8 @@ void _confirmDelete(BuildContext context, double sf, Color cardColor, Color text
),
TextButton(
onPressed: () {
// ⚡ 1. FECHA LOGO O POP-UP!
Navigator.pop(ctx);
// ⚡ 2. AVISA O PAI PARA ESCONDER A EQUIPA DO ECRÃ NA HORA!
onDelete();
// 3. APAGA NO FUNDO (Sem o utilizador ficar à espera)
controller.deleteTeam(team.id).catchError((e) {
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao eliminar: $e'), backgroundColor: Colors.red));
});
@@ -401,7 +406,7 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
File? _selectedImage;
bool _isLoading = false;
bool _isPickerActive = false; // 👇 ESCUDO ANTI-DUPLO-CLIQUE
bool _isPickerActive = false;
Future<void> _pickImage() async {
if (_isPickerActive) return;
@@ -412,7 +417,6 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
final XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
// 👇 USA O CROPPER QUE CONFIGURASTE PARA AS CARAS
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: pickedFile.path,
aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),