This commit is contained in:
2026-03-22 01:40:29 +00:00
parent 6c89b7ab8c
commit 00fee30792
23 changed files with 1717 additions and 2081 deletions

View File

@@ -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))
),
],
),