domingo
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:playmaker/pages/PlacarPage.dart';
|
import 'package:playmaker/pages/PlacarPage.dart';
|
||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart'; // 👇 A MAGIA DO CACHE AQUI
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../controllers/game_controller.dart';
|
import '../controllers/game_controller.dart';
|
||||||
import '../models/game_model.dart';
|
import '../models/game_model.dart';
|
||||||
@@ -43,14 +44,31 @@ class GameResultCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 👇 AVATAR OTIMIZADO COM CACHE 👇
|
||||||
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) {
|
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) {
|
||||||
|
final double avatarSize = 48 * sf; // 2 * radius (24)
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
ClipOval(
|
||||||
radius: 24 * sf,
|
child: Container(
|
||||||
backgroundColor: color,
|
width: avatarSize,
|
||||||
backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null,
|
height: avatarSize,
|
||||||
child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * sf) : null,
|
color: color.withOpacity(0.1), // Fundo suave para não ser agressivo
|
||||||
|
child: (logoUrl != null && logoUrl.isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: logoUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero, // Fica instantâneo
|
||||||
|
placeholder: (context, url) => Center(
|
||||||
|
child: Icon(Icons.shield, color: color, size: 24 * sf)
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => Center(
|
||||||
|
child: Icon(Icons.shield, color: color, size: 24 * sf)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Center(child: Icon(Icons.shield, color: color, size: 24 * sf)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 6 * sf),
|
SizedBox(height: 6 * sf),
|
||||||
Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf, color: textColor), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2),
|
Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf, color: textColor), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2),
|
||||||
@@ -161,6 +179,7 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 👇 PESQUISA COM CACHE 👇
|
||||||
Widget _buildSearch(BuildContext context, String label, TextEditingController controller) {
|
Widget _buildSearch(BuildContext context, String label, TextEditingController controller) {
|
||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: widget.teamController.teamsStream,
|
stream: widget.teamController.teamsStream,
|
||||||
@@ -188,7 +207,22 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|||||||
final String name = option['name'].toString();
|
final String name = option['name'].toString();
|
||||||
final String? imageUrl = option['image_url'];
|
final String? imageUrl = option['image_url'];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: CircleAvatar(radius: 20 * widget.sf, backgroundColor: Colors.grey.withOpacity(0.2), backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * widget.sf) : null),
|
leading: ClipOval(
|
||||||
|
child: Container(
|
||||||
|
width: 40 * widget.sf,
|
||||||
|
height: 40 * widget.sf,
|
||||||
|
color: Colors.grey.withOpacity(0.2),
|
||||||
|
child: (imageUrl != null && imageUrl.isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
placeholder: (context, url) => Icon(Icons.shield, color: Colors.grey, size: 20 * widget.sf),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.shield, color: Colors.grey, size: 20 * widget.sf),
|
||||||
|
)
|
||||||
|
: Icon(Icons.shield, color: Colors.grey, size: 20 * widget.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface)),
|
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * widget.sf, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
onTap: () { onSelected(option); },
|
onTap: () { onSelected(option); },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import 'package:playmaker/pages/teamPage.dart';
|
|||||||
import 'package:playmaker/controllers/team_controller.dart';
|
import 'package:playmaker/controllers/team_controller.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:playmaker/pages/status_page.dart';
|
import 'package:playmaker/pages/status_page.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../utils/size_extension.dart';
|
import '../utils/size_extension.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
// import 'home_widgets.dart';
|
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
@@ -29,17 +30,29 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
|
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
// 👇 NOVA VARIÁVEL PARA GUARDAR A FOTO
|
|
||||||
String? _avatarUrl;
|
String? _avatarUrl;
|
||||||
|
bool _isMemoryLoaded = false; // 👈 A variável mágica que impede o "piscar" inicial
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadUserAvatar(); // Vai buscar a foto logo quando a Home abre!
|
_loadUserAvatar();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👇 FUNÇÃO PARA LER A FOTO DA BASE DE DADOS
|
// 👇 FUNÇÃO OTIMIZADA: Carrega da memória instantaneamente e atualiza em background
|
||||||
Future<void> _loadUserAvatar() async {
|
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) _avatarUrl = savedUrl;
|
||||||
|
_isMemoryLoaded = true; // Avisa o ecrã que a memória já respondeu!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. VAI AO SUPABASE VERIFICAR SE TROCASTE DE FOTO
|
||||||
final userId = _supabase.auth.currentUser?.id;
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
if (userId == null) return;
|
if (userId == null) return;
|
||||||
|
|
||||||
@@ -51,12 +64,18 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (mounted && data != null && data['avatar_url'] != null) {
|
if (mounted && data != null && data['avatar_url'] != null) {
|
||||||
setState(() {
|
final urlDoSupabase = data['avatar_url'];
|
||||||
_avatarUrl = data['avatar_url'];
|
|
||||||
});
|
// Se a foto na base de dados for nova, ele guarda e atualiza!
|
||||||
|
if (urlDoSupabase != savedUrl) {
|
||||||
|
await prefs.setString('meu_avatar_guardado', urlDoSupabase);
|
||||||
|
setState(() {
|
||||||
|
_avatarUrl = urlDoSupabase;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Erro ao carregar avatar na Home: $e");
|
debugPrint("Erro ao carregar avatar na Home: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,33 +91,47 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf)),
|
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold)),
|
||||||
backgroundColor: AppTheme.primaryRed,
|
backgroundColor: AppTheme.primaryRed,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
|
||||||
// 👇 AQUI ESTÁ A MÁGICA DA TUA FOTO NA APPBAR 👇
|
|
||||||
leading: Padding(
|
leading: Padding(
|
||||||
padding: EdgeInsets.all(10.0 * context.sf), // Dá um espacinho para não colar aos bordos
|
padding: EdgeInsets.all(10.0 * context.sf),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(100),
|
borderRadius: BorderRadius.circular(100),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
// O 'await' faz com que a Home espere que tu feches os settings...
|
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
||||||
);
|
);
|
||||||
// ... e quando voltas, ele recarrega a foto logo!
|
|
||||||
_loadUserAvatar();
|
_loadUserAvatar();
|
||||||
},
|
},
|
||||||
child: CircleAvatar(
|
// 👇 SÓ MOSTRA A IMAGEM OU O BONECO DEPOIS DE LER A MEMÓRIA
|
||||||
backgroundColor: Colors.white.withOpacity(0.2), // Fundo suave caso não haja foto
|
child: !_isMemoryLoaded
|
||||||
backgroundImage: _avatarUrl != null && _avatarUrl!.isNotEmpty
|
// Nos primeiros 0.05 segs, mostra só o círculo de fundo (sem boneco)
|
||||||
? NetworkImage(_avatarUrl!)
|
? CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2))
|
||||||
: null,
|
|
||||||
child: _avatarUrl == null || _avatarUrl!.isEmpty
|
// Depois da memória responder:
|
||||||
? Icon(Icons.person, color: Colors.white, size: 20 * context.sf)
|
: _avatarUrl != null && _avatarUrl!.isNotEmpty
|
||||||
: null, // Só mostra o ícone se não houver foto
|
? CachedNetworkImage(
|
||||||
),
|
imageUrl: _avatarUrl!,
|
||||||
|
fadeInDuration: Duration.zero, // Corta o atraso visual!
|
||||||
|
imageBuilder: (context, imageProvider) => CircleAvatar(
|
||||||
|
backgroundColor: Colors.white.withOpacity(0.2),
|
||||||
|
backgroundImage: imageProvider,
|
||||||
|
),
|
||||||
|
placeholder: (context, url) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)),
|
||||||
|
errorWidget: (context, url, error) => CircleAvatar(
|
||||||
|
backgroundColor: Colors.white.withOpacity(0.2),
|
||||||
|
child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// Se não tiver foto nenhuma, aí sim mostra o boneco
|
||||||
|
: CircleAvatar(
|
||||||
|
backgroundColor: Colors.white.withOpacity(0.2),
|
||||||
|
child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -144,14 +177,15 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final team = teams[index];
|
final team = teams[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
leading: const Icon(Icons.shield, color: AppTheme.primaryRed),
|
||||||
|
title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedTeamId = team['id'];
|
_selectedTeamId = team['id'].toString();
|
||||||
_selectedTeamName = team['name'];
|
_selectedTeamName = team['name'];
|
||||||
_teamWins = team['wins'] != null ? int.tryParse(team['wins'].toString()) ?? 0 : 0;
|
_teamWins = int.tryParse(team['wins']?.toString() ?? '0') ?? 0;
|
||||||
_teamLosses = team['losses'] != null ? int.tryParse(team['losses'].toString()) ?? 0 : 0;
|
_teamLosses = int.tryParse(team['losses']?.toString() ?? '0') ?? 0;
|
||||||
_teamDraws = team['draws'] != null ? int.tryParse(team['draws'].toString()) ?? 0 : 0;
|
_teamDraws = int.tryParse(team['draws']?.toString() ?? '0') ?? 0;
|
||||||
});
|
});
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
@@ -251,9 +285,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
color: Theme.of(context).cardTheme.color ?? Colors.white,
|
color: Theme.of(context).cardTheme.color ?? Colors.white,
|
||||||
borderRadius: BorderRadius.circular(16 * context.sf),
|
borderRadius: BorderRadius.circular(16 * context.sf),
|
||||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||||
boxShadow: [
|
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: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -263,10 +295,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
child: Icon(Icons.shield_outlined, color: AppTheme.primaryRed, size: 42 * context.sf),
|
child: Icon(Icons.shield_outlined, color: AppTheme.primaryRed, size: 42 * context.sf),
|
||||||
),
|
),
|
||||||
SizedBox(height: 20 * context.sf),
|
SizedBox(height: 20 * context.sf),
|
||||||
Text(
|
Text("Nenhuma Equipa Ativa", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: textColor)),
|
||||||
"Nenhuma Equipa Ativa",
|
|
||||||
style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8 * context.sf),
|
SizedBox(height: 8 * context.sf),
|
||||||
Text(
|
Text(
|
||||||
"Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.",
|
"Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.",
|
||||||
@@ -293,8 +322,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: StreamBuilder<List<Map<String, dynamic>>>(
|
: StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: _supabase.from('games').stream(primaryKey: ['id'])
|
stream: _supabase.from('games').stream(primaryKey: ['id']).order('game_date', ascending: false),
|
||||||
.order('game_date', ascending: false),
|
|
||||||
builder: (context, gameSnapshot) {
|
builder: (context, gameSnapshot) {
|
||||||
if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
|
if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
|
||||||
if (gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
|
if (gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
|
||||||
@@ -309,6 +337,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
|
|
||||||
if (gamesList.isEmpty) {
|
if (gamesList.isEmpty) {
|
||||||
return Container(
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
padding: EdgeInsets.all(20 * context.sf),
|
padding: EdgeInsets.all(20 * context.sf),
|
||||||
decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(14)),
|
decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(14)),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
@@ -362,9 +391,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
for (var row in data) {
|
for (var row in data) {
|
||||||
String pid = row['member_id'].toString();
|
String pid = row['member_id'].toString();
|
||||||
namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido";
|
namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido";
|
||||||
ptsMap[pid] = (ptsMap[pid] ?? 0) + (row['pts'] as int? ?? 0);
|
ptsMap[pid] = (ptsMap[pid] ?? 0) + (int.tryParse(row['pts']?.toString() ?? '0') ?? 0);
|
||||||
astMap[pid] = (astMap[pid] ?? 0) + (row['ast'] as int? ?? 0);
|
astMap[pid] = (astMap[pid] ?? 0) + (int.tryParse(row['ast']?.toString() ?? '0') ?? 0);
|
||||||
rbsMap[pid] = (rbsMap[pid] ?? 0) + (row['rbs'] as int? ?? 0);
|
rbsMap[pid] = (rbsMap[pid] ?? 0) + (int.tryParse(row['rbs']?.toString() ?? '0') ?? 0);
|
||||||
}
|
}
|
||||||
if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0};
|
if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0};
|
||||||
String getBest(Map<String, int> map) { var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key; return namesMap[bestId]!; }
|
String getBest(Map<String, int> map) { var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key; return namesMap[bestId]!; }
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:image_picker/image_picker.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 '../utils/size_extension.dart';
|
||||||
import 'login.dart'; // 👇 Necessário para o redirecionamento do logout
|
import 'login.dart';
|
||||||
import '../main.dart'; // 👇 OBRIGATÓRIO PARA LER A VARIÁVEL "themeNotifier"
|
import '../main.dart';
|
||||||
|
|
||||||
class SettingsScreen extends StatefulWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
const SettingsScreen({super.key});
|
const SettingsScreen({super.key});
|
||||||
@@ -18,10 +20,10 @@ class SettingsScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _SettingsScreenState extends State<SettingsScreen> {
|
class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
|
|
||||||
// 👇 VARIÁVEIS DE ESTADO PARA FOTO DE PERFIL
|
|
||||||
File? _localImageFile;
|
File? _localImageFile;
|
||||||
String? _uploadedImageUrl;
|
String? _uploadedImageUrl;
|
||||||
bool _isUploadingImage = false;
|
bool _isUploadingImage = false;
|
||||||
|
bool _isMemoryLoaded = false; // 👇 VARIÁVEL MÁGICA CONTRA O PISCAR
|
||||||
|
|
||||||
final supabase = Supabase.instance.client;
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
@@ -31,41 +33,52 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_loadUserAvatar();
|
_loadUserAvatar();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👇 LÊ A IMAGEM ATUAL DA BASE DE DADOS (Tabela 'profiles')
|
// 👇 LÊ A IMAGEM DA MEMÓRIA INSTANTANEAMENTE E CONFIRMA NA BD
|
||||||
void _loadUserAvatar() async {
|
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;
|
final userId = supabase.auth.currentUser?.id;
|
||||||
if (userId == null) return;
|
if (userId == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ⚠️ NOTA: Ajusta 'profiles' e 'avatar_url' se os nomes na tua BD forem diferentes!
|
|
||||||
final data = await supabase
|
final data = await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.select('avatar_url')
|
.select('avatar_url')
|
||||||
.eq('id', userId)
|
.eq('id', userId)
|
||||||
.maybeSingle(); // maybeSingle evita erro se o perfil ainda não existir
|
.maybeSingle();
|
||||||
|
|
||||||
if (mounted && data != null && data['avatar_url'] != null) {
|
if (mounted && data != null && data['avatar_url'] != null) {
|
||||||
setState(() {
|
final urlDoSupabase = data['avatar_url'];
|
||||||
_uploadedImageUrl = 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) {
|
} catch (e) {
|
||||||
print("Erro ao carregar avatar: $e");
|
print("Erro ao carregar avatar: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// 👇 A MÁGICA DE ESCOLHER E FAZER UPLOAD DA FOTO 👇
|
|
||||||
// =========================================================================
|
|
||||||
Future<void> _handleImageChange() async {
|
Future<void> _handleImageChange() async {
|
||||||
final ImagePicker picker = ImagePicker();
|
final ImagePicker picker = ImagePicker();
|
||||||
|
|
||||||
// 1. ABRIR GALERIA
|
|
||||||
final XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
|
final XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
|
||||||
if (pickedFile == null || !mounted) return;
|
if (pickedFile == null || !mounted) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 2. MOSTRAR IMAGEM LOCAL E ATIVAR LOADING
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_localImageFile = File(pickedFile.path);
|
_localImageFile = File(pickedFile.path);
|
||||||
_isUploadingImage = true;
|
_isUploadingImage = true;
|
||||||
@@ -76,26 +89,25 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
final String storagePath = '$userId/profile_picture.png';
|
final String storagePath = '$userId/profile_picture.png';
|
||||||
|
|
||||||
// 3. FAZER UPLOAD (Método direto e seguro!)
|
|
||||||
await supabase.storage.from('avatars').upload(
|
await supabase.storage.from('avatars').upload(
|
||||||
storagePath,
|
storagePath,
|
||||||
_localImageFile!, // Envia o ficheiro File diretamente!
|
_localImageFile!,
|
||||||
fileOptions: const FileOptions(cacheControl: '3600', upsert: true)
|
fileOptions: const FileOptions(cacheControl: '3600', upsert: true)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. OBTER URL PÚBLICO
|
|
||||||
final String publicUrl = supabase.storage.from('avatars').getPublicUrl(storagePath);
|
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
|
await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.upsert({
|
.upsert({
|
||||||
'id': userId, // Garante que atualiza o perfil certo ou cria um novo
|
'id': userId,
|
||||||
'avatar_url': publicUrl
|
'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) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_uploadedImageUrl = publicUrl;
|
_uploadedImageUrl = publicUrl;
|
||||||
@@ -151,9 +163,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// ==========================================
|
|
||||||
// CARTÃO DE PERFIL
|
|
||||||
// ==========================================
|
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.all(20 * context.sf),
|
padding: EdgeInsets.all(20 * context.sf),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -166,7 +175,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 👇 IMAGEM TAPPABLE AQUI 👇
|
|
||||||
_buildTappableProfileAvatar(context, primaryRed),
|
_buildTappableProfileAvatar(context, primaryRed),
|
||||||
SizedBox(width: 16 * context.sf),
|
SizedBox(width: 16 * context.sf),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -191,9 +199,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
SizedBox(height: 32 * context.sf),
|
SizedBox(height: 32 * context.sf),
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// SECÇÃO: DEFINIÇÕES
|
|
||||||
// ==========================================
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 4 * context.sf, bottom: 12 * context.sf),
|
padding: EdgeInsets.only(left: 4 * context.sf, bottom: 12 * context.sf),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -233,9 +238,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
SizedBox(height: 32 * context.sf),
|
SizedBox(height: 32 * context.sf),
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// SECÇÃO: CONTA
|
|
||||||
// ==========================================
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 4 * context.sf, bottom: 12 * context.sf),
|
padding: EdgeInsets.only(left: 4 * context.sf, bottom: 12 * context.sf),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -265,9 +267,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
SizedBox(height: 50 * context.sf),
|
SizedBox(height: 50 * context.sf),
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// VERSÃO DA APP
|
|
||||||
// ==========================================
|
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"PlayMaker v1.0.0",
|
"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) {
|
Widget _buildTappableProfileAvatar(BuildContext context, Color primaryRed) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
print("CLIQUEI NA FOTO! A abrir galeria..."); // 👇 Vê na consola se isto aparece
|
|
||||||
_handleImageChange();
|
_handleImageChange();
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
Container(
|
||||||
radius: 36 * context.sf,
|
width: 72 * context.sf,
|
||||||
backgroundColor: primaryRed.withOpacity(0.1),
|
height: 72 * context.sf,
|
||||||
backgroundImage: _isUploadingImage && _localImageFile != null
|
decoration: BoxDecoration(
|
||||||
? FileImage(_localImageFile!)
|
color: primaryRed.withOpacity(0.1),
|
||||||
: (_uploadedImageUrl != null && _uploadedImageUrl!.isNotEmpty
|
shape: BoxShape.circle,
|
||||||
? NetworkImage(_uploadedImageUrl!)
|
),
|
||||||
: null),
|
child: ClipOval(
|
||||||
child: (_uploadedImageUrl == null && !(_isUploadingImage && _localImageFile != null))
|
child: _isUploadingImage && _localImageFile != null
|
||||||
? Icon(Icons.person, color: primaryRed, size: 36 * context.sf)
|
// 1. Mostrar imagem local (galeria) ENQUANTO está a fazer upload
|
||||||
: null,
|
? 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
|
// ÍCONE DE LÁPIS
|
||||||
@@ -319,7 +335,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// LOADING OVERLAY
|
// LOADING OVERLAY (Enquanto faz o upload)
|
||||||
if (_isUploadingImage)
|
if (_isUploadingImage)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -335,7 +351,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👇 FUNÇÃO DE LOGOUT
|
|
||||||
void _confirmLogout(BuildContext context) {
|
void _confirmLogout(BuildContext context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -349,6 +364,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||||
onPressed: () async {
|
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();
|
await Supabase.instance.client.auth.signOut();
|
||||||
if (ctx.mounted) {
|
if (ctx.mounted) {
|
||||||
Navigator.of(ctx).pushAndRemoveUntil(
|
Navigator.of(ctx).pushAndRemoveUntil(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart'; // 👇 A MAGIA DO CACHE
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../utils/size_extension.dart';
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
|
stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
|
||||||
builder: (context, membersSnapshot) {
|
builder: (context, membersSnapshot) {
|
||||||
if (statsSnapshot.connectionState == ConnectionState.waiting || gamesSnapshot.connectionState == ConnectionState.waiting || membersSnapshot.connectionState == ConnectionState.waiting) {
|
if (statsSnapshot.connectionState == ConnectionState.waiting || gamesSnapshot.connectionState == ConnectionState.waiting || membersSnapshot.connectionState == ConnectionState.waiting) {
|
||||||
return Center(child: CircularProgressIndicator(color: AppTheme.primaryRed));
|
return const Center(child: CircularProgressIndicator(color: AppTheme.primaryRed));
|
||||||
}
|
}
|
||||||
|
|
||||||
final membersData = membersSnapshot.data ?? [];
|
final membersData = membersSnapshot.data ?? [];
|
||||||
@@ -98,15 +99,17 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 👇 AGORA GUARDA TAMBÉM O IMAGE_URL DO MEMBRO PARA MOSTRAR NA TABELA
|
||||||
List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
|
List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
|
||||||
Map<String, Map<String, dynamic>> aggregated = {};
|
Map<String, Map<String, dynamic>> aggregated = {};
|
||||||
for (var member in members) {
|
for (var member in members) {
|
||||||
String name = member['name']?.toString() ?? "Desconhecido";
|
String name = member['name']?.toString() ?? "Desconhecido";
|
||||||
aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
|
String? imageUrl = member['image_url']?.toString(); // 👈 CAPTURA A IMAGEM AQUI
|
||||||
|
aggregated[name] = {'name': name, 'image_url': imageUrl, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
|
||||||
}
|
}
|
||||||
for (var row in stats) {
|
for (var row in stats) {
|
||||||
String name = row['player_name']?.toString() ?? "Desconhecido";
|
String name = row['player_name']?.toString() ?? "Desconhecido";
|
||||||
if (!aggregated.containsKey(name)) aggregated[name] = {'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
|
if (!aggregated.containsKey(name)) aggregated[name] = {'name': name, 'image_url': null, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
|
||||||
|
|
||||||
aggregated[name]!['j'] += 1;
|
aggregated[name]!['j'] += 1;
|
||||||
aggregated[name]!['pts'] += (row['pts'] ?? 0);
|
aggregated[name]!['pts'] += (row['pts'] ?? 0);
|
||||||
@@ -132,7 +135,7 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
for (var p in players) {
|
for (var p in players) {
|
||||||
tPts += (p['pts'] as int); tAst += (p['ast'] as int); tRbs += (p['rbs'] as int); tStl += (p['stl'] as int); tBlk += (p['blk'] as int); tMvp += (p['mvp'] as int); tDef += (p['def'] as int);
|
tPts += (p['pts'] as int); tAst += (p['ast'] as int); tRbs += (p['rbs'] as int); tStl += (p['stl'] as int); tBlk += (p['blk'] as int); tMvp += (p['mvp'] as int); tDef += (p['def'] as int);
|
||||||
}
|
}
|
||||||
return {'name': 'TOTAL EQUIPA', 'j': teamGames, 'pts': tPts, 'ast': tAst, 'rbs': tRbs, 'stl': tStl, 'blk': tBlk, 'mvp': tMvp, 'def': tDef};
|
return {'name': 'TOTAL EQUIPA', 'image_url': null, 'j': teamGames, 'pts': tPts, 'ast': tAst, 'rbs': tRbs, 'stl': tStl, 'blk': tBlk, 'mvp': tMvp, 'def': tDef};
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, Color bgColor, Color textColor) {
|
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, Color bgColor, Color textColor) {
|
||||||
@@ -160,7 +163,31 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
],
|
],
|
||||||
rows: [
|
rows: [
|
||||||
...players.map((player) => DataRow(cells: [
|
...players.map((player) => DataRow(cells: [
|
||||||
DataCell(Row(children: [CircleAvatar(radius: 15 * context.sf, backgroundColor: Colors.grey.withOpacity(0.2), child: Icon(Icons.person, size: 18 * context.sf, color: Colors.grey)), SizedBox(width: 10 * context.sf), Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor))])),
|
DataCell(
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// 👇 FOTO DO JOGADOR NA TABELA (COM CACHE!) 👇
|
||||||
|
ClipOval(
|
||||||
|
child: Container(
|
||||||
|
width: 30 * context.sf,
|
||||||
|
height: 30 * context.sf,
|
||||||
|
color: Colors.grey.withOpacity(0.2),
|
||||||
|
child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: player['image_url'],
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
placeholder: (context, url) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
||||||
|
)
|
||||||
|
: Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10 * context.sf),
|
||||||
|
Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))),
|
DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))),
|
||||||
_buildStatCell(context, player['pts'], textColor, isHighlight: true),
|
_buildStatCell(context, player['pts'], textColor, isHighlight: true),
|
||||||
_buildStatCell(context, player['ast'], textColor),
|
_buildStatCell(context, player['ast'], textColor),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:image_cropper/image_cropper.dart';
|
import 'package:image_cropper/image_cropper.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart'; // 👇 A MAGIA DO CACHE AQUI
|
||||||
import 'package:playmaker/screens/team_stats_page.dart';
|
import 'package:playmaker/screens/team_stats_page.dart';
|
||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
@@ -172,7 +173,6 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👇 AGORA USA FUTUREBUILDER E É MUITO MAIS RÁPIDO 👇
|
|
||||||
Widget _buildTeamsList() {
|
Widget _buildTeamsList() {
|
||||||
return FutureBuilder<List<Map<String, dynamic>>>(
|
return FutureBuilder<List<Map<String, dynamic>>>(
|
||||||
future: controller.getTeamsWithStats(),
|
future: controller.getTeamsWithStats(),
|
||||||
@@ -196,7 +196,7 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
color: AppTheme.primaryRed,
|
color: AppTheme.primaryRed,
|
||||||
onRefresh: () async => setState(() {}), // Puxa para baixo para recarregar
|
onRefresh: () async => setState(() {}),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf),
|
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf),
|
||||||
itemCount: data.length,
|
itemCount: data.length,
|
||||||
@@ -209,9 +209,9 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
onFavoriteTap: () async {
|
onFavoriteTap: () async {
|
||||||
await controller.toggleFavorite(team.id, team.isFavorite);
|
await controller.toggleFavorite(team.id, team.isFavorite);
|
||||||
setState(() {}); // Atualiza a estrela na hora
|
setState(() {});
|
||||||
},
|
},
|
||||||
onDelete: () => setState(() {}), // Atualiza a lista quando apaga
|
onDelete: () => setState(() {}),
|
||||||
sf: context.sf,
|
sf: context.sf,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -229,7 +229,7 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
sf: context.sf,
|
sf: context.sf,
|
||||||
onConfirm: (name, season, imageFile) async {
|
onConfirm: (name, season, imageFile) async {
|
||||||
await controller.createTeam(name, season, imageFile);
|
await controller.createTeam(name, season, imageFile);
|
||||||
setState(() {}); // 👇 Atualiza a lista quando acaba de criar a equipa!
|
setState(() {});
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -241,7 +241,7 @@ class TeamCard extends StatelessWidget {
|
|||||||
final Team team;
|
final Team team;
|
||||||
final TeamController controller;
|
final TeamController controller;
|
||||||
final VoidCallback onFavoriteTap;
|
final VoidCallback onFavoriteTap;
|
||||||
final VoidCallback onDelete; // 👇 Avisa o pai quando é apagado
|
final VoidCallback onDelete;
|
||||||
final double sf;
|
final double sf;
|
||||||
|
|
||||||
const TeamCard({
|
const TeamCard({
|
||||||
@@ -257,6 +257,7 @@ class TeamCard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
|
final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
|
||||||
final textColor = Theme.of(context).colorScheme.onSurface;
|
final textColor = Theme.of(context).colorScheme.onSurface;
|
||||||
|
final double avatarSize = 56 * sf; // 2 * radius (28)
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsets.only(bottom: 12 * sf),
|
margin: EdgeInsets.only(bottom: 12 * sf),
|
||||||
@@ -274,18 +275,29 @@ class TeamCard extends StatelessWidget {
|
|||||||
leading: Stack(
|
leading: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
// 👇 AVATAR DA EQUIPA OTIMIZADO COM CACHE 👇
|
||||||
radius: 28 * sf,
|
ClipOval(
|
||||||
backgroundColor: Colors.grey.withOpacity(0.2),
|
child: Container(
|
||||||
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
|
width: avatarSize,
|
||||||
? NetworkImage(team.imageUrl)
|
height: avatarSize,
|
||||||
: null,
|
color: Colors.grey.withOpacity(0.2),
|
||||||
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
|
child: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
|
||||||
? Text(
|
? CachedNetworkImage(
|
||||||
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
|
imageUrl: team.imageUrl,
|
||||||
style: TextStyle(fontSize: 24 * sf),
|
fit: BoxFit.cover,
|
||||||
)
|
fadeInDuration: Duration.zero, // Fica instantâneo no scroll
|
||||||
: null,
|
placeholder: (context, url) => const SizedBox(), // Fica só o fundo cinza
|
||||||
|
errorWidget: (context, url, error) => Center(
|
||||||
|
child: Text("🏀", style: TextStyle(fontSize: 24 * sf)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Text(
|
||||||
|
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
|
||||||
|
style: TextStyle(fontSize: 24 * sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: -15 * sf,
|
left: -15 * sf,
|
||||||
@@ -313,8 +325,6 @@ class TeamCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
|
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
|
||||||
SizedBox(width: 4 * sf),
|
SizedBox(width: 4 * sf),
|
||||||
|
|
||||||
// 👇 ESTATÍSTICA MUITO MAIS LEVE. LÊ O VALOR DIRETAMENTE! 👇
|
|
||||||
Text(
|
Text(
|
||||||
"${team.playerCount} Jogs.",
|
"${team.playerCount} Jogs.",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -323,7 +333,6 @@ class TeamCard extends StatelessWidget {
|
|||||||
fontSize: 13 * sf,
|
fontSize: 13 * sf,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(width: 8 * sf),
|
SizedBox(width: 8 * sf),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * sf), overflow: TextOverflow.ellipsis),
|
child: Text("| ${team.season}", style: TextStyle(color: Colors.grey, fontSize: 13 * sf), overflow: TextOverflow.ellipsis),
|
||||||
@@ -337,7 +346,7 @@ class TeamCard extends StatelessWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Ver Estatísticas',
|
tooltip: 'Ver Estatísticas',
|
||||||
icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * sf),
|
icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * sf),
|
||||||
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))).then((_) => onDelete()), // Atualiza se algo mudou
|
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => TeamStatsPage(team: team))).then((_) => onDelete()),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Eliminar Equipa',
|
tooltip: 'Eliminar Equipa',
|
||||||
@@ -351,7 +360,7 @@ 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(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
@@ -366,12 +375,8 @@ void _confirmDelete(BuildContext context, double sf, Color cardColor, Color text
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// ⚡ 1. FECHA LOGO O POP-UP!
|
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
// ⚡ 2. AVISA O PAI PARA ESCONDER A EQUIPA DO ECRÃ NA HORA!
|
|
||||||
onDelete();
|
onDelete();
|
||||||
|
|
||||||
// 3. APAGA NO FUNDO (Sem o utilizador ficar à espera)
|
|
||||||
controller.deleteTeam(team.id).catchError((e) {
|
controller.deleteTeam(team.id).catchError((e) {
|
||||||
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao eliminar: $e'), backgroundColor: Colors.red));
|
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao eliminar: $e'), backgroundColor: Colors.red));
|
||||||
});
|
});
|
||||||
@@ -401,7 +406,7 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
|
|||||||
|
|
||||||
File? _selectedImage;
|
File? _selectedImage;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _isPickerActive = false; // 👇 ESCUDO ANTI-DUPLO-CLIQUE
|
bool _isPickerActive = false;
|
||||||
|
|
||||||
Future<void> _pickImage() async {
|
Future<void> _pickImage() async {
|
||||||
if (_isPickerActive) return;
|
if (_isPickerActive) return;
|
||||||
@@ -412,7 +417,6 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
|
|||||||
final XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
|
final XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
|
||||||
|
|
||||||
if (pickedFile != null) {
|
if (pickedFile != null) {
|
||||||
// 👇 USA O CROPPER QUE CONFIGURASTE PARA AS CARAS
|
|
||||||
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||||
sourcePath: pickedFile.path,
|
sourcePath: pickedFile.path,
|
||||||
aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),
|
aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:image_cropper/image_cropper.dart';
|
import 'package:image_cropper/image_cropper.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart'; // 👇 MAGIA DO CACHE AQUI
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import '../models/team_model.dart';
|
import '../models/team_model.dart';
|
||||||
@@ -11,7 +11,7 @@ import '../models/person_model.dart';
|
|||||||
import '../utils/size_extension.dart';
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 1. CABEÇALHO (AGORA COM CACHE DE IMAGEM)
|
// 1. CABEÇALHO (AGORA COM CACHE DE IMAGEM INSTANTÂNEO)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
class StatsHeader extends StatelessWidget {
|
class StatsHeader extends StatelessWidget {
|
||||||
final Team team;
|
final Team team;
|
||||||
@@ -51,15 +51,27 @@ class StatsHeader extends StatelessWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
// 👇 AVATAR DA EQUIPA SEM LAG 👇
|
||||||
radius: 28 * context.sf,
|
ClipOval(
|
||||||
backgroundColor: Colors.white24,
|
child: Container(
|
||||||
backgroundImage: (currentImageUrl != null && currentImageUrl!.isNotEmpty && currentImageUrl!.startsWith('http'))
|
width: 56 * context.sf,
|
||||||
? CachedNetworkImageProvider(currentImageUrl!)
|
height: 56 * context.sf,
|
||||||
: null,
|
color: Colors.white24,
|
||||||
child: (currentImageUrl == null || currentImageUrl!.isEmpty || !currentImageUrl!.startsWith('http'))
|
child: (currentImageUrl != null && currentImageUrl!.isNotEmpty && currentImageUrl!.startsWith('http'))
|
||||||
? Text((currentImageUrl != null && currentImageUrl!.isNotEmpty) ? currentImageUrl! : "🛡️", style: TextStyle(fontSize: 24 * context.sf))
|
? CachedNetworkImage(
|
||||||
: null,
|
imageUrl: currentImageUrl!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero, // Corta o atraso
|
||||||
|
placeholder: (context, url) => Center(child: Text("🛡️", style: TextStyle(fontSize: 24 * context.sf))),
|
||||||
|
errorWidget: (context, url, error) => Center(child: Text("🛡️", style: TextStyle(fontSize: 24 * context.sf))),
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Text(
|
||||||
|
(currentImageUrl != null && currentImageUrl!.isNotEmpty) ? currentImageUrl! : "🛡️",
|
||||||
|
style: TextStyle(fontSize: 24 * context.sf)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0, right: 0,
|
bottom: 0, right: 0,
|
||||||
@@ -144,7 +156,7 @@ class StatsSectionTitle extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CARD DA PESSOA (FOTO + NÚMERO + NOME E CACHE) ---
|
// --- CARD DA PESSOA (FOTO SEM LAG) ---
|
||||||
class PersonCard extends StatelessWidget {
|
class PersonCard extends StatelessWidget {
|
||||||
final Person person;
|
final Person person;
|
||||||
final bool isCoach;
|
final bool isCoach;
|
||||||
@@ -158,6 +170,7 @@ class PersonCard extends StatelessWidget {
|
|||||||
final Color defaultBg = Theme.of(context).brightness == Brightness.dark ? const Color(0xFF1E1E1E) : Colors.white;
|
final Color defaultBg = Theme.of(context).brightness == Brightness.dark ? const Color(0xFF1E1E1E) : Colors.white;
|
||||||
final Color coachBg = Theme.of(context).brightness == Brightness.dark ? AppTheme.warningAmber.withOpacity(0.1) : const Color(0xFFFFF9C4);
|
final Color coachBg = Theme.of(context).brightness == Brightness.dark ? AppTheme.warningAmber.withOpacity(0.1) : const Color(0xFFFFF9C4);
|
||||||
final String? pImage = person.imageUrl;
|
final String? pImage = person.imageUrl;
|
||||||
|
final Color iconColor = isCoach ? Colors.white : AppTheme.primaryRed;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: EdgeInsets.only(top: 12 * context.sf),
|
margin: EdgeInsets.only(top: 12 * context.sf),
|
||||||
@@ -168,11 +181,22 @@ class PersonCard extends StatelessWidget {
|
|||||||
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf),
|
padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
// 👇 FOTO DO JOGADOR/TREINADOR INSTANTÂNEA 👇
|
||||||
radius: 22 * context.sf,
|
ClipOval(
|
||||||
backgroundColor: isCoach ? AppTheme.warningAmber : AppTheme.primaryRed.withOpacity(0.1),
|
child: Container(
|
||||||
backgroundImage: (pImage != null && pImage.isNotEmpty) ? CachedNetworkImageProvider(pImage) : null,
|
width: 44 * context.sf,
|
||||||
child: (pImage == null || pImage.isEmpty) ? Icon(Icons.person, color: isCoach ? Colors.white : AppTheme.primaryRed, size: 24 * context.sf) : null,
|
height: 44 * context.sf,
|
||||||
|
color: isCoach ? AppTheme.warningAmber : AppTheme.primaryRed.withOpacity(0.1),
|
||||||
|
child: (pImage != null && pImage.isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: pImage,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
placeholder: (context, url) => Icon(Icons.person, color: iconColor, size: 24 * context.sf),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.person, color: iconColor, size: 24 * context.sf),
|
||||||
|
)
|
||||||
|
: Icon(Icons.person, color: iconColor, size: 24 * context.sf),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 12 * context.sf),
|
SizedBox(width: 12 * context.sf),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -385,12 +409,9 @@ void _confirmDelete(BuildContext context, Person person) {
|
|||||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancelar", style: TextStyle(color: Colors.grey))),
|
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancelar", style: TextStyle(color: Colors.grey))),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// ⚡ FECHA LOGO O POP-UP!
|
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
// Mostra um aviso rápido para o utilizador saber que a app está a trabalhar
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("A remover ${person.name}..."), duration: const Duration(seconds: 1)));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("A remover ${person.name}..."), duration: const Duration(seconds: 1)));
|
||||||
|
|
||||||
// APAGA NO FUNDO
|
|
||||||
_controller.deletePerson(person).catchError((e) {
|
_controller.deletePerson(person).catchError((e) {
|
||||||
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Erro: $e"), backgroundColor: AppTheme.primaryRed));
|
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Erro: $e"), backgroundColor: AppTheme.primaryRed));
|
||||||
});
|
});
|
||||||
@@ -480,7 +501,6 @@ class StatsController {
|
|||||||
bool isPickerActive = false;
|
bool isPickerActive = false;
|
||||||
String? currentImageUrl = isEdit ? person.imageUrl : null;
|
String? currentImageUrl = isEdit ? person.imageUrl : null;
|
||||||
|
|
||||||
// 👇 VARIÁVEIS PARA O TEXTO PEQUENO VERMELHO (ESTILO LOGIN) 👇
|
|
||||||
String? nameError;
|
String? nameError;
|
||||||
String? numError;
|
String? numError;
|
||||||
|
|
||||||
@@ -512,15 +532,24 @@ class StatsController {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
// 👇 PREVIEW DA FOTO NO POPUP SEM LAG 👇
|
||||||
radius: 40 * context.sf,
|
ClipOval(
|
||||||
backgroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.05),
|
child: Container(
|
||||||
backgroundImage: selectedImage != null
|
width: 80 * context.sf,
|
||||||
? FileImage(selectedImage!)
|
height: 80 * context.sf,
|
||||||
: (currentImageUrl != null && currentImageUrl!.isNotEmpty ? CachedNetworkImageProvider(currentImageUrl!) : null) as ImageProvider?,
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.05),
|
||||||
child: (selectedImage == null && (currentImageUrl == null || currentImageUrl!.isEmpty))
|
child: selectedImage != null
|
||||||
? Icon(Icons.add_a_photo, size: 30 * context.sf, color: Colors.grey)
|
? Image.file(selectedImage!, fit: BoxFit.cover)
|
||||||
: null,
|
: (currentImageUrl != null && currentImageUrl!.isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: currentImageUrl!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
placeholder: (context, url) => Icon(Icons.add_a_photo, size: 30 * context.sf, color: Colors.grey),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.add_a_photo, size: 30 * context.sf, color: Colors.grey),
|
||||||
|
)
|
||||||
|
: Icon(Icons.add_a_photo, size: 30 * context.sf, color: Colors.grey),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0, right: 0,
|
bottom: 0, right: 0,
|
||||||
@@ -540,7 +569,7 @@ class StatsController {
|
|||||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Nome Completo",
|
labelText: "Nome Completo",
|
||||||
errorText: nameError, // 👇 ERRO PEQUENO AQUI
|
errorText: nameError,
|
||||||
),
|
),
|
||||||
textCapitalization: TextCapitalization.words,
|
textCapitalization: TextCapitalization.words,
|
||||||
),
|
),
|
||||||
@@ -560,7 +589,7 @@ class StatsController {
|
|||||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Número da Camisola",
|
labelText: "Número da Camisola",
|
||||||
errorText: numError, // 👇 ERRO PEQUENO AQUI
|
errorText: numError,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
),
|
),
|
||||||
@@ -574,7 +603,6 @@ class StatsController {
|
|||||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.successGreen, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8 * context.sf))),
|
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.successGreen, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8 * context.sf))),
|
||||||
onPressed: isUploading ? null : () async {
|
onPressed: isUploading ? null : () async {
|
||||||
|
|
||||||
// Limpa os erros antes de tentar de novo
|
|
||||||
setState(() {
|
setState(() {
|
||||||
nameError = null;
|
nameError = null;
|
||||||
numError = null;
|
numError = null;
|
||||||
@@ -621,7 +649,6 @@ class StatsController {
|
|||||||
}
|
}
|
||||||
if (ctx.mounted) Navigator.pop(ctx);
|
if (ctx.mounted) Navigator.pop(ctx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 👇 AGORA OS ERROS VÃO DIRETOS PARA OS CAMPOS (ESTILO LOGIN) 👇
|
|
||||||
setState(() {
|
setState(() {
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
if (e is PostgrestException && e.code == '23505') {
|
if (e is PostgrestException && e.code == '23505') {
|
||||||
|
|||||||
Reference in New Issue
Block a user