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

@@ -4,10 +4,12 @@ import 'package:flutter/material.dart';
import 'package:playmaker/classe/theme.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:image_picker/image_picker.dart';
import 'package:cached_network_image/cached_network_image.dart'; // 👇 IMPORTAÇÃO PARA CACHE
import 'package:shared_preferences/shared_preferences.dart'; // 👇 IMPORTAÇÃO PARA MEMÓRIA RÁPIDA
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"
import 'login.dart';
import '../main.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@@ -18,10 +20,10 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> {
// 👇 VARIÁVEIS DE ESTADO PARA FOTO DE PERFIL
File? _localImageFile;
String? _uploadedImageUrl;
bool _isUploadingImage = false;
bool _isMemoryLoaded = false; // 👇 VARIÁVEL MÁGICA CONTRA O PISCAR
final supabase = Supabase.instance.client;
@@ -31,41 +33,52 @@ class _SettingsScreenState extends State<SettingsScreen> {
_loadUserAvatar();
}
// 👇 LÊ A IMAGEM ATUAL DA BASE DE DADOS (Tabela 'profiles')
void _loadUserAvatar() async {
// 👇 LÊ A IMAGEM DA MEMÓRIA INSTANTANEAMENTE E CONFIRMA NA BD
Future<void> _loadUserAvatar() async {
// 1. Lê da memória rápida primeiro!
final prefs = await SharedPreferences.getInstance();
final savedUrl = prefs.getString('meu_avatar_guardado');
if (mounted) {
setState(() {
if (savedUrl != null) _uploadedImageUrl = savedUrl;
_isMemoryLoaded = true; // Avisa que já leu a memória
});
}
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
.maybeSingle();
if (mounted && data != null && data['avatar_url'] != null) {
setState(() {
_uploadedImageUrl = data['avatar_url'];
});
final urlDoSupabase = data['avatar_url'];
// Atualiza a memória se a foto na base de dados for diferente
if (urlDoSupabase != savedUrl) {
await prefs.setString('meu_avatar_guardado', urlDoSupabase);
setState(() {
_uploadedImageUrl = urlDoSupabase;
});
}
}
} 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;
@@ -76,26 +89,25 @@ class _SettingsScreenState extends State<SettingsScreen> {
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!
_localImageFile!,
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
'id': userId,
'avatar_url': publicUrl
});
// 6. SUCESSO!
// 👇 MÁGICA: GUARDA LOGO O NOVO URL NA MEMÓRIA PARA A HOME SABER!
final prefs = await SharedPreferences.getInstance();
await prefs.setString('meu_avatar_guardado', publicUrl);
if (mounted) {
setState(() {
_uploadedImageUrl = publicUrl;
@@ -151,9 +163,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ==========================================
// CARTÃO DE PERFIL
// ==========================================
Container(
padding: EdgeInsets.all(20 * context.sf),
decoration: BoxDecoration(
@@ -166,7 +175,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
child: Row(
children: [
// 👇 IMAGEM TAPPABLE AQUI 👇
_buildTappableProfileAvatar(context, primaryRed),
SizedBox(width: 16 * context.sf),
Expanded(
@@ -191,9 +199,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
SizedBox(height: 32 * context.sf),
// ==========================================
// SECÇÃO: DEFINIÇÕES
// ==========================================
Padding(
padding: EdgeInsets.only(left: 4 * context.sf, bottom: 12 * context.sf),
child: Text(
@@ -233,9 +238,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
SizedBox(height: 32 * context.sf),
// ==========================================
// SECÇÃO: CONTA
// ==========================================
Padding(
padding: EdgeInsets.only(left: 4 * context.sf, bottom: 12 * context.sf),
child: Text(
@@ -265,9 +267,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
SizedBox(height: 50 * context.sf),
// ==========================================
// VERSÃO DA APP
// ==========================================
Center(
child: Text(
"PlayMaker v1.0.0",
@@ -281,27 +280,44 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
// 👇 O WIDGET DA FOTO DE PERFIL (Protegido com GestureDetector)
// 👇 AVATAR OTIMIZADO: SEM LAG, COM CACHE E MEMÓRIA
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,
Container(
width: 72 * context.sf,
height: 72 * context.sf,
decoration: BoxDecoration(
color: primaryRed.withOpacity(0.1),
shape: BoxShape.circle,
),
child: ClipOval(
child: _isUploadingImage && _localImageFile != null
// 1. Mostrar imagem local (galeria) ENQUANTO está a fazer upload
? Image.file(_localImageFile!, fit: BoxFit.cover)
// 2. Antes da memória carregar, fica só o fundo (evita piscar)
: !_isMemoryLoaded
? const SizedBox()
// 3. Depois da memória carregar, se houver URL, desenha com Cache!
: _uploadedImageUrl != null && _uploadedImageUrl!.isNotEmpty
? CachedNetworkImage(
imageUrl: _uploadedImageUrl!,
fit: BoxFit.cover,
fadeInDuration: Duration.zero, // Fica instantâneo!
placeholder: (context, url) => const SizedBox(),
errorWidget: (context, url, error) => Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
)
// 4. Se não houver URL, mete o boneco
: Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
),
),
// ÍCONE DE LÁPIS
@@ -319,7 +335,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
// LOADING OVERLAY
// LOADING OVERLAY (Enquanto faz o upload)
if (_isUploadingImage)
Positioned.fill(
child: Container(
@@ -335,7 +351,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
// 👇 FUNÇÃO DE LOGOUT
void _confirmLogout(BuildContext context) {
showDialog(
context: context,
@@ -349,6 +364,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
onPressed: () async {
// Limpa a memória do Avatar ao sair para não aparecer na conta de outra pessoa!
final prefs = await SharedPreferences.getInstance();
await prefs.remove('meu_avatar_guardado');
await Supabase.instance.client.auth.signOut();
if (ctx.mounted) {
Navigator.of(ctx).pushAndRemoveUntil(