sabado
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:playmaker/screens/team_stats_page.dart';
|
||||
import 'package:playmaker/classe/theme.dart'; // 👇 IMPORT DO TEMA
|
||||
import 'package:playmaker/classe/theme.dart';
|
||||
import '../controllers/team_controller.dart';
|
||||
import '../models/team_model.dart';
|
||||
import '../utils/size_extension.dart';
|
||||
@@ -162,16 +165,17 @@ class _TeamsPageState extends State<TeamsPage> {
|
||||
hintStyle: TextStyle(fontSize: 16 * context.sf, color: Colors.grey),
|
||||
prefixIcon: Icon(Icons.search, color: AppTheme.primaryRed, size: 22 * context.sf),
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.surface, // 👇 Adapta-se ao Dark Mode
|
||||
fillColor: Theme.of(context).colorScheme.surface,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15 * context.sf), borderSide: BorderSide.none),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 👇 AGORA USA FUTUREBUILDER E É MUITO MAIS RÁPIDO 👇
|
||||
Widget _buildTeamsList() {
|
||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||
stream: controller.teamsStream,
|
||||
return FutureBuilder<List<Map<String, dynamic>>>(
|
||||
future: controller.getTeamsWithStats(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) return Center(child: CircularProgressIndicator(color: AppTheme.primaryRed));
|
||||
if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text("Nenhuma equipa encontrada.", style: TextStyle(fontSize: 16 * context.sf, color: Theme.of(context).colorScheme.onSurface)));
|
||||
@@ -190,28 +194,45 @@ class _TeamsPageState extends State<TeamsPage> {
|
||||
else return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString());
|
||||
});
|
||||
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf),
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final team = Team.fromMap(data[index]);
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))),
|
||||
child: TeamCard(
|
||||
team: team,
|
||||
controller: controller,
|
||||
onFavoriteTap: () => controller.toggleFavorite(team.id, team.isFavorite),
|
||||
sf: context.sf,
|
||||
),
|
||||
);
|
||||
},
|
||||
return RefreshIndicator(
|
||||
color: AppTheme.primaryRed,
|
||||
onRefresh: () async => setState(() {}), // Puxa para baixo para recarregar
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf),
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final team = Team.fromMap(data[index]);
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))).then((_) => setState(() {})),
|
||||
child: TeamCard(
|
||||
team: team,
|
||||
controller: controller,
|
||||
onFavoriteTap: () async {
|
||||
await controller.toggleFavorite(team.id, team.isFavorite);
|
||||
setState(() {}); // Atualiza a estrela na hora
|
||||
},
|
||||
onDelete: () => setState(() {}), // Atualiza a lista quando apaga
|
||||
sf: context.sf,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
sf: context.sf,
|
||||
onConfirm: (name, season, imageFile) async {
|
||||
await controller.createTeam(name, season, imageFile);
|
||||
setState(() {}); // 👇 Atualiza a lista quando acaba de criar a equipa!
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +241,7 @@ class TeamCard extends StatelessWidget {
|
||||
final Team team;
|
||||
final TeamController controller;
|
||||
final VoidCallback onFavoriteTap;
|
||||
final VoidCallback onDelete; // 👇 Avisa o pai quando é apagado
|
||||
final double sf;
|
||||
|
||||
const TeamCard({
|
||||
@@ -227,6 +249,7 @@ class TeamCard extends StatelessWidget {
|
||||
required this.team,
|
||||
required this.controller,
|
||||
required this.onFavoriteTap,
|
||||
required this.onDelete,
|
||||
required this.sf,
|
||||
});
|
||||
|
||||
@@ -259,7 +282,7 @@ class TeamCard extends StatelessWidget {
|
||||
: null,
|
||||
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
|
||||
? Text(
|
||||
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
|
||||
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
|
||||
style: TextStyle(fontSize: 24 * sf),
|
||||
)
|
||||
: null,
|
||||
@@ -272,9 +295,7 @@ class TeamCard extends StatelessWidget {
|
||||
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),
|
||||
],
|
||||
shadows: [Shadow(color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), blurRadius: 4 * sf)],
|
||||
),
|
||||
onPressed: onFavoriteTap,
|
||||
),
|
||||
@@ -292,21 +313,17 @@ class TeamCard extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
// 👇 ESTATÍSTICA MUITO MAIS LEVE. LÊ O VALOR DIRETAMENTE! 👇
|
||||
Text(
|
||||
"${team.playerCount} Jogs.",
|
||||
style: TextStyle(
|
||||
color: team.playerCount > 0 ? AppTheme.successGreen : AppTheme.warningAmber,
|
||||
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),
|
||||
@@ -320,7 +337,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))),
|
||||
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))).then((_) => onDelete()), // Atualiza se algo mudou
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Eliminar Equipa',
|
||||
@@ -334,23 +351,30 @@ 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: (context) => AlertDialog(
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: cardColor,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold, color: textColor)),
|
||||
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * sf, color: textColor)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf, color: Colors.grey)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
controller.deleteTeam(team.id);
|
||||
Navigator.pop(context);
|
||||
// ⚡ 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));
|
||||
});
|
||||
},
|
||||
child: Text('Eliminar', style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf)),
|
||||
),
|
||||
@@ -360,9 +384,9 @@ class TeamCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// --- DIALOG DE CRIAÇÃO ---
|
||||
// --- DIALOG DE CRIAÇÃO (COM CROPPER E ESCUDO) ---
|
||||
class CreateTeamDialog extends StatefulWidget {
|
||||
final Function(String name, String season, String imageUrl) onConfirm;
|
||||
final Function(String name, String season, File? imageFile) onConfirm;
|
||||
final double sf;
|
||||
|
||||
const CreateTeamDialog({super.key, required this.onConfirm, required this.sf});
|
||||
@@ -373,8 +397,48 @@ class CreateTeamDialog extends StatefulWidget {
|
||||
|
||||
class _CreateTeamDialogState extends State<CreateTeamDialog> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _imageController = TextEditingController();
|
||||
String _selectedSeason = '2024/25';
|
||||
|
||||
File? _selectedImage;
|
||||
bool _isLoading = false;
|
||||
bool _isPickerActive = false; // 👇 ESCUDO ANTI-DUPLO-CLIQUE
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
if (_isPickerActive) return;
|
||||
setState(() => _isPickerActive = true);
|
||||
|
||||
try {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
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),
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: 'Recortar Logo',
|
||||
toolbarColor: AppTheme.primaryRed,
|
||||
toolbarWidgetColor: Colors.white,
|
||||
initAspectRatio: CropAspectRatioPreset.square,
|
||||
lockAspectRatio: true,
|
||||
hideBottomControls: true,
|
||||
),
|
||||
IOSUiSettings(title: 'Recortar Logo', aspectRatioLockEnabled: true, resetButtonHidden: true),
|
||||
],
|
||||
);
|
||||
|
||||
if (croppedFile != null && mounted) {
|
||||
setState(() {
|
||||
_selectedImage = File(croppedFile.path);
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isPickerActive = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -386,6 +450,34 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 40 * widget.sf,
|
||||
backgroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.05),
|
||||
backgroundImage: _selectedImage != null ? FileImage(_selectedImage!) : null,
|
||||
child: _selectedImage == null
|
||||
? Icon(Icons.add_photo_alternate_outlined, size: 30 * widget.sf, color: Colors.grey)
|
||||
: null,
|
||||
),
|
||||
if (_selectedImage == null)
|
||||
Positioned(
|
||||
bottom: 0, right: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(4 * widget.sf),
|
||||
decoration: const BoxDecoration(color: AppTheme.primaryRed, shape: BoxShape.circle),
|
||||
child: Icon(Icons.add, color: Colors.white, size: 16 * widget.sf),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10 * widget.sf),
|
||||
Text("Logótipo (Opcional)", style: TextStyle(fontSize: 12 * widget.sf, color: Colors.grey)),
|
||||
SizedBox(height: 20 * widget.sf),
|
||||
|
||||
TextField(controller: _nameController, style: TextStyle(fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration(labelText: 'Nome da Equipa', labelStyle: TextStyle(fontSize: 14 * widget.sf)), textCapitalization: TextCapitalization.words),
|
||||
SizedBox(height: 15 * widget.sf),
|
||||
DropdownButtonFormField<String>(
|
||||
@@ -395,8 +487,6 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
|
||||
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, 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))),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -404,8 +494,16 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)),
|
||||
onPressed: () { if (_nameController.text.trim().isNotEmpty) { widget.onConfirm(_nameController.text.trim(), _selectedSeason, _imageController.text.trim()); Navigator.pop(context); } },
|
||||
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)),
|
||||
onPressed: _isLoading ? null : () async {
|
||||
if (_nameController.text.trim().isNotEmpty) {
|
||||
setState(() => _isLoading = true);
|
||||
await widget.onConfirm(_nameController.text.trim(), _selectedSeason, _selectedImage);
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: _isLoading
|
||||
? SizedBox(width: 16 * widget.sf, height: 16 * widget.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user