sabado
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,6 @@ import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:playmaker/pages/status_page.dart';
|
||||
import '../utils/size_extension.dart';
|
||||
import 'settings_screen.dart';
|
||||
// 👇 Importa o ficheiro onde meteste o StatCard e o SportGrid
|
||||
// import 'home_widgets.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
@@ -29,6 +28,37 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
int _teamDraws = 0;
|
||||
|
||||
final _supabase = Supabase.instance.client;
|
||||
|
||||
// 👇 NOVA VARIÁVEL PARA GUARDAR A FOTO
|
||||
String? _avatarUrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUserAvatar(); // Vai buscar a foto logo quando a Home abre!
|
||||
}
|
||||
|
||||
// 👇 FUNÇÃO PARA LER A FOTO DA BASE DE DADOS
|
||||
Future<void> _loadUserAvatar() async {
|
||||
final userId = _supabase.auth.currentUser?.id;
|
||||
if (userId == null) return;
|
||||
|
||||
try {
|
||||
final data = await _supabase
|
||||
.from('profiles')
|
||||
.select('avatar_url')
|
||||
.eq('id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (mounted && data != null && data['avatar_url'] != null) {
|
||||
setState(() {
|
||||
_avatarUrl = data['avatar_url'];
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print("Erro ao carregar avatar na Home: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -45,14 +75,31 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)),
|
||||
backgroundColor: AppTheme.primaryRed,
|
||||
foregroundColor: Colors.white,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.person, size: 24 * context.sf),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
||||
);
|
||||
},
|
||||
|
||||
// 👇 AQUI ESTÁ A MÁGICA DA TUA FOTO NA APPBAR 👇
|
||||
leading: Padding(
|
||||
padding: EdgeInsets.all(10.0 * context.sf), // Dá um espacinho para não colar aos bordos
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
onTap: () async {
|
||||
// O 'await' faz com que a Home espere que tu feches os settings...
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
||||
);
|
||||
// ... e quando voltas, ele recarrega a foto logo!
|
||||
_loadUserAvatar();
|
||||
},
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.white.withOpacity(0.2), // Fundo suave caso não haja foto
|
||||
backgroundImage: _avatarUrl != null && _avatarUrl!.isNotEmpty
|
||||
? NetworkImage(_avatarUrl!)
|
||||
: null,
|
||||
child: _avatarUrl == null || _avatarUrl!.isEmpty
|
||||
? Icon(Icons.person, color: Colors.white, size: 20 * context.sf)
|
||||
: null, // Só mostra o ícone se não houver foto
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -196,7 +243,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: textColor)),
|
||||
SizedBox(height: 16 * context.sf),
|
||||
|
||||
// 👇 AQUI ESTÁ O NOVO CARTÃO VAZIO PARA QUANDO NÃO HÁ EQUIPA 👇
|
||||
_selectedTeamName == "Selecionar Equipa"
|
||||
? Container(
|
||||
width: double.infinity,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:playmaker/classe/theme.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../utils/size_extension.dart';
|
||||
import 'login.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
// 👇 OBRIGATÓRIO IMPORTAR O MAIN.DART PARA LER A VARIÁVEL "themeNotifier"
|
||||
import '../main.dart';
|
||||
import '../utils/size_extension.dart';
|
||||
import 'login.dart'; // 👇 Necessário para o redirecionamento do logout
|
||||
import '../main.dart'; // 👇 OBRIGATÓRIO PARA LER A VARIÁVEL "themeNotifier"
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
@@ -16,16 +18,116 @@ class SettingsScreen extends StatefulWidget {
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
// 👇 VARIÁVEIS DE ESTADO PARA FOTO DE PERFIL
|
||||
File? _localImageFile;
|
||||
String? _uploadedImageUrl;
|
||||
bool _isUploadingImage = false;
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUserAvatar();
|
||||
}
|
||||
|
||||
// 👇 LÊ A IMAGEM ATUAL DA BASE DE DADOS (Tabela 'profiles')
|
||||
void _loadUserAvatar() async {
|
||||
final userId = supabase.auth.currentUser?.id;
|
||||
if (userId == null) return;
|
||||
|
||||
try {
|
||||
// ⚠️ NOTA: Ajusta 'profiles' e 'avatar_url' se os nomes na tua BD forem diferentes!
|
||||
final data = await supabase
|
||||
.from('profiles')
|
||||
.select('avatar_url')
|
||||
.eq('id', userId)
|
||||
.maybeSingle(); // maybeSingle evita erro se o perfil ainda não existir
|
||||
|
||||
if (mounted && data != null && data['avatar_url'] != null) {
|
||||
setState(() {
|
||||
_uploadedImageUrl = data['avatar_url'];
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print("Erro ao carregar avatar: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 👇 A MÁGICA DE ESCOLHER E FAZER UPLOAD DA FOTO 👇
|
||||
// =========================================================================
|
||||
Future<void> _handleImageChange() async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
|
||||
// 1. ABRIR GALERIA
|
||||
final XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
|
||||
if (pickedFile == null || !mounted) return;
|
||||
|
||||
try {
|
||||
// 2. MOSTRAR IMAGEM LOCAL E ATIVAR LOADING
|
||||
setState(() {
|
||||
_localImageFile = File(pickedFile.path);
|
||||
_isUploadingImage = true;
|
||||
});
|
||||
|
||||
final userId = supabase.auth.currentUser?.id;
|
||||
if (userId == null) throw Exception("Utilizador não autenticado.");
|
||||
|
||||
final String storagePath = '$userId/profile_picture.png';
|
||||
|
||||
// 3. FAZER UPLOAD (Método direto e seguro!)
|
||||
await supabase.storage.from('avatars').upload(
|
||||
storagePath,
|
||||
_localImageFile!, // Envia o ficheiro File diretamente!
|
||||
fileOptions: const FileOptions(cacheControl: '3600', upsert: true)
|
||||
);
|
||||
|
||||
// 4. OBTER URL PÚBLICO
|
||||
final String publicUrl = supabase.storage.from('avatars').getPublicUrl(storagePath);
|
||||
|
||||
// 5. ATUALIZAR NA BASE DE DADOS
|
||||
// ⚠️ NOTA: Garante que a tabela 'profiles' existe e tem o teu user_id
|
||||
await supabase
|
||||
.from('profiles')
|
||||
.upsert({
|
||||
'id': userId, // Garante que atualiza o perfil certo ou cria um novo
|
||||
'avatar_url': publicUrl
|
||||
});
|
||||
|
||||
// 6. SUCESSO!
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_uploadedImageUrl = publicUrl;
|
||||
_isUploadingImage = false;
|
||||
_localImageFile = null;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Foto atualizada!"), backgroundColor: Colors.green)
|
||||
);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isUploadingImage = false;
|
||||
_localImageFile = null;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Erro: $e"), backgroundColor: AppTheme.primaryRed)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 👇 CORES DINÂMICAS (A MÁGICA DO MODO ESCURO)
|
||||
final Color primaryRed = AppTheme.primaryRed;
|
||||
final Color bgColor = Theme.of(context).scaffoldBackgroundColor;
|
||||
final Color cardColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
|
||||
final Color textColor = Theme.of(context).colorScheme.onSurface;
|
||||
final Color textLightColor = textColor.withOpacity(0.6);
|
||||
|
||||
// 👇 SABER SE A APP ESTÁ ESCURA OU CLARA NESTE EXATO MOMENTO
|
||||
bool isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Scaffold(
|
||||
@@ -37,10 +139,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
"Perfil e Definições",
|
||||
style: TextStyle(
|
||||
fontSize: 18 * context.sf,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.w600),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
@@ -62,20 +161,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
borderRadius: BorderRadius.circular(16 * context.sf),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 32 * context.sf,
|
||||
backgroundColor: primaryRed.withOpacity(0.1),
|
||||
child: Icon(Icons.person, color: primaryRed, size: 32 * context.sf),
|
||||
),
|
||||
// 👇 IMAGEM TAPPABLE AQUI 👇
|
||||
_buildTappableProfileAvatar(context, primaryRed),
|
||||
SizedBox(width: 16 * context.sf),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -83,19 +175,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
children: [
|
||||
Text(
|
||||
"Treinador",
|
||||
style: TextStyle(
|
||||
fontSize: 18 * context.sf,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: textColor),
|
||||
),
|
||||
SizedBox(height: 4 * context.sf),
|
||||
Text(
|
||||
Supabase.instance.client.auth.currentUser?.email ?? "sem@email.com",
|
||||
style: TextStyle(
|
||||
color: textLightColor,
|
||||
fontSize: 14 * context.sf,
|
||||
),
|
||||
supabase.auth.currentUser?.email ?? "sem@email.com",
|
||||
style: TextStyle(color: textLightColor, fontSize: 14 * context.sf),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -113,11 +198,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
padding: EdgeInsets.only(left: 4 * context.sf, bottom: 12 * context.sf),
|
||||
child: Text(
|
||||
"Definições",
|
||||
style: TextStyle(
|
||||
color: textLightColor,
|
||||
fontSize: 14 * context.sf,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: TextStyle(color: textLightColor, fontSize: 14 * context.sf, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
@@ -126,11 +207,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
borderRadius: BorderRadius.circular(16 * context.sf),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
@@ -148,7 +225,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
value: isDark,
|
||||
activeColor: primaryRed,
|
||||
onChanged: (bool value) {
|
||||
// 👇 CHAMA A VARIÁVEL DO MAIN.DART E ATUALIZA A APP TODA
|
||||
themeNotifier.value = value ? ThemeMode.dark : ThemeMode.light;
|
||||
},
|
||||
),
|
||||
@@ -164,11 +240,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
padding: EdgeInsets.only(left: 4 * context.sf, bottom: 12 * context.sf),
|
||||
child: Text(
|
||||
"Conta",
|
||||
style: TextStyle(
|
||||
color: textLightColor,
|
||||
fontSize: 14 * context.sf,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: TextStyle(color: textLightColor, fontSize: 14 * context.sf, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
@@ -177,11 +249,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
borderRadius: BorderRadius.circular(16 * context.sf),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
@@ -189,13 +257,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
leading: Icon(Icons.logout_outlined, color: primaryRed, size: 26 * context.sf),
|
||||
title: Text(
|
||||
"Terminar Sessão",
|
||||
style: TextStyle(
|
||||
color: primaryRed,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15 * context.sf,
|
||||
),
|
||||
style: TextStyle(color: primaryRed, fontWeight: FontWeight.bold, fontSize: 15 * context.sf),
|
||||
),
|
||||
onTap: () => _confirmLogout(context), // 👇 CHAMA O LOGOUT REAL
|
||||
onTap: () => _confirmLogout(context),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -207,10 +271,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Center(
|
||||
child: Text(
|
||||
"PlayMaker v1.0.0",
|
||||
style: TextStyle(
|
||||
color: textLightColor.withOpacity(0.7),
|
||||
fontSize: 13 * context.sf,
|
||||
),
|
||||
style: TextStyle(color: textLightColor.withOpacity(0.7), fontSize: 13 * context.sf),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20 * context.sf),
|
||||
@@ -220,28 +281,83 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// 👇 FUNÇÃO PARA FAZER LOGOUT
|
||||
// 👇 O WIDGET DA FOTO DE PERFIL (Protegido com GestureDetector)
|
||||
Widget _buildTappableProfileAvatar(BuildContext context, Color primaryRed) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
print("CLIQUEI NA FOTO! A abrir galeria..."); // 👇 Vê na consola se isto aparece
|
||||
_handleImageChange();
|
||||
},
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 36 * context.sf,
|
||||
backgroundColor: primaryRed.withOpacity(0.1),
|
||||
backgroundImage: _isUploadingImage && _localImageFile != null
|
||||
? FileImage(_localImageFile!)
|
||||
: (_uploadedImageUrl != null && _uploadedImageUrl!.isNotEmpty
|
||||
? NetworkImage(_uploadedImageUrl!)
|
||||
: null),
|
||||
child: (_uploadedImageUrl == null && !(_isUploadingImage && _localImageFile != null))
|
||||
? Icon(Icons.person, color: primaryRed, size: 36 * context.sf)
|
||||
: null,
|
||||
),
|
||||
|
||||
// ÍCONE DE LÁPIS
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(6 * context.sf),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
),
|
||||
child: Icon(Icons.edit_outlined, color: primaryRed, size: 16 * context.sf),
|
||||
),
|
||||
),
|
||||
|
||||
// LOADING OVERLAY
|
||||
if (_isUploadingImage)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: Colors.black.withOpacity(0.4), shape: BoxShape.circle),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 3),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 👇 FUNÇÃO DE LOGOUT
|
||||
void _confirmLogout(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
title: Text("Terminar Sessão", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16 * context.sf)),
|
||||
title: Text("Terminar Sessão", style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||
content: Text("Tens a certeza que queres sair da conta?", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancelar", style: TextStyle(color: Colors.grey))),
|
||||
TextButton(
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
onPressed: () async {
|
||||
await Supabase.instance.client.auth.signOut();
|
||||
if (ctx.mounted) {
|
||||
// Mata a navegação toda para trás e manda para o Login
|
||||
Navigator.of(ctx).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (context) => const LoginPage()),
|
||||
(Route<dynamic> route) => false,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text("Sair", style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold))
|
||||
child: const Text("Sair", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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