domingo
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user