eliminar e tentativa de partilhar

This commit is contained in:
2026-04-04 01:28:47 +01:00
parent 1b08ed7d07
commit 2544e52636
5 changed files with 169 additions and 56 deletions

View File

@@ -79,7 +79,19 @@ class GameController {
print("Erro ao criar jogo: $e");
return null;
}
}
}
// ELIMINAR JOGO
Future<bool> deleteGame(String gameId) async {
try {
await _supabase.from('games').delete().eq('id', gameId);
// Como o Supabase tem Cascade Delete (se configurado), vai apagar também
// as stats e shot_locations associadas a este game_id automaticamente.
return true;
} catch (e) {
print("Erro ao eliminar jogo: $e");
return false;
}
}
void dispose() {}
}

View File

@@ -78,7 +78,7 @@ class PlacarController extends ChangeNotifier {
String? pendingPlayerId;
List<ShotRecord> matchShots = [];
// 👇 LISTA PARA O HISTÓRICO (PLAY-BY-PLAY)
// Lista para o Histórico de Jogadas
List<String> playByPlay = [];
ValueNotifier<Duration> durationNotifier = ValueNotifier(const Duration(minutes: 10));
@@ -113,6 +113,13 @@ class PlacarController extends ChangeNotifier {
gameWasAlreadyFinished = gameResponse['status'] == 'Terminado';
// CARREGAR HISTÓRICO DA BASE DE DADOS
if (gameResponse['play_by_play'] != null) {
playByPlay = List<String>.from(gameResponse['play_by_play']);
} else {
playByPlay = [];
}
final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
for (var t in teamsResponse) {
if (t['name'] == myTeam) myTeamDbId = t['id'];
@@ -224,7 +231,7 @@ class PlacarController extends ChangeNotifier {
'playerStats': playerStats,
'myCourt': myCourt, 'myBench': myBench, 'oppCourt': oppCourt, 'oppBench': oppBench,
'matchShots': matchShots.map((s) => s.toJson()).toList(),
'playByPlay': playByPlay, // 👇 Guarda o histórico
'playByPlay': playByPlay, // Guarda o histórico no telemóvel
};
await prefs.setString('backup_$gameId', jsonEncode(backupData));
} catch (e) {
@@ -254,7 +261,7 @@ class PlacarController extends ChangeNotifier {
List<dynamic> decodedShots = data['matchShots'];
matchShots = decodedShots.map((s) => ShotRecord.fromJson(s)).toList();
playByPlay = List<String>.from(data['playByPlay'] ?? []); // 👇 Carrega o histórico
playByPlay = List<String>.from(data['playByPlay'] ?? []);
debugPrint("🔄 AUTO-SAVE RECUPERADO COM SUCESSO!");
}
@@ -346,23 +353,22 @@ class PlacarController extends ChangeNotifier {
void registerShotFromPopup(BuildContext context, String action, String targetPlayer, String zone, int points, double relativeX, double relativeY) {
String playerId = targetPlayer.replaceAll("player_my_", "").replaceAll("player_opp_", "");
bool isMyTeam = targetPlayer.startsWith("player_my_");
bool isMake = action.startsWith("add_");
String name = playerNames[playerId]!;
String name = playerNames[playerId] ?? "Jogador";
if (playerStats.containsKey(playerId)) {
playerStats[playerId]!['fga'] = playerStats[playerId]!['fga']! + 1;
matchShots.add(ShotRecord(
relativeX: relativeX,
relativeY: relativeY,
isMake: isMake,
playerId: playerId,
playerName: name,
zone: zone,
points: points
));
if (isMake) {
playerStats[playerId]!['fgm'] = playerStats[playerId]!['fgm']! + 1;
playerStats[playerId]!['pts'] = playerStats[playerId]!['pts']! + points;
if (isMyTeam) myScore += points; else opponentScore += points;
}
}
String finalAction = isMake ? "add_pts_$points" : "miss_$points";
commitStat(finalAction, targetPlayer);
matchShots.add(ShotRecord(relativeX: relativeX, relativeY: relativeY, isMake: isMake, playerId: playerId, playerName: name, zone: zone, points: points));
_saveLocalBackup();
notifyListeners();
}
@@ -457,7 +463,18 @@ class PlacarController extends ChangeNotifier {
else if (action == "miss_2" || action == "miss_3") { stats["fga"] = stats["fga"]! + 1; logText = "falhou lançamento ❌"; }
else if (action == "add_orb") { stats["orb"] = stats["orb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; logText = "ganhou ressalto ofensivo 🔄"; }
else if (action == "add_drb") { stats["drb"] = stats["drb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; logText = "ganhou ressalto defensivo 🛡️"; }
else if (action == "add_ast") { stats["ast"] = stats["ast"]! + 1; logText = "fez uma assistência 🤝"; }
else if (action == "add_ast") {
stats["ast"] = stats["ast"]! + 1;
if (playByPlay.isNotEmpty && playByPlay[0].contains("marcou") && !playByPlay[0].contains("Assistência")) {
playByPlay[0] = "${playByPlay[0]} (Assistência: $name 🤝)";
_saveLocalBackup();
notifyListeners();
return;
} else {
logText = "fez uma assistência 🤝";
}
}
else if (action == "add_stl") { stats["stl"] = stats["stl"]! + 1; logText = "roubou a bola 🥷"; }
else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; logText = "perdeu a bola (turnover) 🤦"; }
else if (action == "add_blk") { stats["blk"] = stats["blk"]! + 1; logText = "fez um desarme (bloco) ✋"; }
@@ -514,6 +531,7 @@ class PlacarController extends ChangeNotifier {
if (mvpScore > maxMvpScore && mvpScore > 0) { maxMvpScore = mvpScore; mvpName = pName; }
});
// ATUALIZA O JOGO COM OS NOVOS ESTADOS E COM O HISTÓRICO DE JOGADAS!
await supabase.from('games').update({
'my_score': myScore,
'opponent_score': opponentScore,
@@ -527,6 +545,7 @@ class PlacarController extends ChangeNotifier {
'top_rbs_name': topRbsName,
'top_def_name': topDefName,
'mvp_name': mvpName,
'play_by_play': playByPlay, // Envia o histórico para a base de dados
}).eq('id', gameId);
if (isGameFinishedNow && !gameWasAlreadyFinished && myTeamDbId != null && oppTeamDbId != null) {

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:playmaker/pages/PlacarPage.dart';
import 'package:playmaker/classe/theme.dart';
import 'package:cached_network_image/cached_network_image.dart'; // 👇 A MAGIA DO CACHE AQUI
import 'package:cached_network_image/cached_network_image.dart';
import '../controllers/team_controller.dart';
import '../controllers/game_controller.dart';
import '../models/game_model.dart';
@@ -12,16 +12,17 @@ class GameResultCard extends StatelessWidget {
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
final String? myTeamLogo, opponentTeamLogo;
final double sf;
final VoidCallback onDelete;
const GameResultCard({
super.key, required this.gameId, required this.myTeam, required this.opponentTeam,
required this.myScore, required this.opponentScore, required this.status, required this.season,
this.myTeamLogo, this.opponentTeamLogo, required this.sf,
this.myTeamLogo, this.opponentTeamLogo, required this.sf, required this.onDelete,
});
@override
Widget build(BuildContext context) {
final bgColor = Theme.of(context).cardTheme.color;
final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
final textColor = Theme.of(context).colorScheme.onSurface;
return Container(
@@ -33,7 +34,9 @@ class GameResultCard extends StatelessWidget {
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
child: Row(
child: Stack(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: _buildTeamInfo(myTeam, AppTheme.primaryRed, myTeamLogo, sf, textColor)),
@@ -41,12 +44,47 @@ class GameResultCard extends StatelessWidget {
Expanded(child: _buildTeamInfo(opponentTeam, Colors.grey.shade600, opponentTeamLogo, sf, textColor)),
],
),
Positioned(
top: -10 * sf,
right: -10 * sf,
child: IconButton(
icon: Icon(Icons.delete_outline, color: Colors.grey.shade400, size: 22 * sf),
splashRadius: 20 * sf,
onPressed: () => _showDeleteConfirmation(context),
),
),
],
),
);
}
void _showDeleteConfirmation(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)),
title: Text('Eliminar Jogo', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf, color: Theme.of(context).colorScheme.onSurface)),
content: Text('Tem a certeza que deseja eliminar este jogo? Esta ação apagará todas as estatísticas associadas e não pode ser desfeita.', style: TextStyle(fontSize: 14 * sf, color: Theme.of(context).colorScheme.onSurface)),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text('CANCELAR', style: TextStyle(color: Colors.grey, fontSize: 14 * sf))
),
TextButton(
onPressed: () {
Navigator.pop(ctx);
onDelete();
},
child: Text('ELIMINAR', style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * sf))
),
],
)
);
}
// 👇 AVATAR OTIMIZADO COM CACHE 👇
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) {
final double avatarSize = 48 * sf; // 2 * radius (24)
final double avatarSize = 48 * sf;
return Column(
children: [
@@ -54,18 +92,14 @@ class GameResultCard extends StatelessWidget {
child: Container(
width: avatarSize,
height: avatarSize,
color: color.withOpacity(0.1), // Fundo suave para não ser agressivo
color: color.withOpacity(0.1),
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)
),
fadeInDuration: Duration.zero,
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)),
),
@@ -179,7 +213,6 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
);
}
// 👇 PESQUISA COM CACHE 👇
Widget _buildSearch(BuildContext context, String label, TextEditingController controller) {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: widget.teamController.teamsStream,
@@ -304,9 +337,33 @@ class _GamePageState extends State<GamePage> {
if (team['name'] == game.opponentTeam) oppLogo = team['image_url'];
}
return GameResultCard(
gameId: game.id, myTeam: game.myTeam, opponentTeam: game.opponentTeam, myScore: game.myScore,
opponentScore: game.opponentScore, status: game.status, season: game.season, myTeamLogo: myLogo, opponentTeamLogo: oppLogo,
gameId: game.id,
myTeam: game.myTeam,
opponentTeam: game.opponentTeam,
myScore: game.myScore,
opponentScore: game.opponentScore,
status: game.status,
season: game.season,
myTeamLogo: myLogo,
opponentTeamLogo: oppLogo,
sf: context.sf,
onDelete: () async {
bool success = await gameController.deleteGame(game.id);
if (context.mounted) {
if (success) {
// 👇 ISTO FORÇA A LISTA A ATUALIZAR IMEDIATAMENTE 👇
setState(() {});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Jogo eliminado com sucesso!'), backgroundColor: Colors.green)
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Erro ao eliminar o jogo.'), backgroundColor: Colors.red)
);
}
}
},
);
},
);

View File

@@ -31,7 +31,7 @@ class _HomeScreenState extends State<HomeScreen> {
final _supabase = Supabase.instance.client;
String? _avatarUrl;
bool _isMemoryLoaded = false; // 👈 A variável mágica que impede o "piscar" inicial
bool _isMemoryLoaded = false; // A variável mágica que impede o "piscar" inicial
@override
void initState() {
@@ -39,7 +39,7 @@ class _HomeScreenState extends State<HomeScreen> {
_loadUserAvatar();
}
// 👇 FUNÇÃO OTIMIZADA: Carrega da memória instantaneamente e atualiza em background
// FUNÇÃO OTIMIZADA: Carrega da memória instantaneamente e atualiza em background
Future<void> _loadUserAvatar() async {
// 1. LÊ DA MEMÓRIA RÁPIDA PRIMEIRO
final prefs = await SharedPreferences.getInstance();
@@ -107,7 +107,7 @@ class _HomeScreenState extends State<HomeScreen> {
);
_loadUserAvatar();
},
// 👇 SÓ MOSTRA A IMAGEM OU O BONECO DEPOIS DE LER A MEMÓRIA
// SÓ MOSTRA A IMAGEM OU O BONECO DEPOIS DE LER A MEMÓRIA
child: !_isMemoryLoaded
// Nos primeiros 0.05 segs, mostra só o círculo de fundo (sem boneco)
? CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2))
@@ -167,8 +167,13 @@ class _HomeScreenState extends State<HomeScreen> {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: _teamController.teamsStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface))));
// Correção: Verifica hasData para evitar piscar tela de loading
if (!snapshot.hasData && snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface))));
}
final teams = snapshot.data!;
return ListView.builder(
@@ -178,11 +183,11 @@ class _HomeScreenState extends State<HomeScreen> {
final team = teams[index];
return ListTile(
leading: const Icon(Icons.shield, color: AppTheme.primaryRed),
title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
title: Text(team['name'] ?? 'Sem Nome', style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
onTap: () {
setState(() {
_selectedTeamId = team['id'].toString();
_selectedTeamName = team['name'];
_selectedTeamName = team['name'] ?? 'Desconhecido';
_teamWins = int.tryParse(team['wins']?.toString() ?? '0') ?? 0;
_teamLosses = int.tryParse(team['losses']?.toString() ?? '0') ?? 0;
_teamDraws = int.tryParse(team['draws']?.toString() ?? '0') ?? 0;
@@ -325,7 +330,11 @@ class _HomeScreenState extends State<HomeScreen> {
stream: _supabase.from('games').stream(primaryKey: ['id']).order('game_date', ascending: false),
builder: (context, gameSnapshot) {
if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
if (gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
// Correção: Verifica hasData em vez de ConnectionState para manter a lista na tela enquanto atualiza em plano de fundo
if (!gameSnapshot.hasData && gameSnapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final todosOsJogos = gameSnapshot.data ?? [];
final gamesList = todosOsJogos.where((game) {
@@ -349,8 +358,8 @@ class _HomeScreenState extends State<HomeScreen> {
children: gamesList.map((game) {
String dbMyTeam = game['my_team']?.toString() ?? '';
String dbOppTeam = game['opponent_team']?.toString() ?? '';
int dbMyScore = int.tryParse(game['my_score'].toString()) ?? 0;
int dbOppScore = int.tryParse(game['opponent_score'].toString()) ?? 0;
int dbMyScore = int.tryParse(game['my_score']?.toString() ?? '0') ?? 0;
int dbOppScore = int.tryParse(game['opponent_score']?.toString() ?? '0') ?? 0;
String opponent; int myScore; int oppScore;
@@ -389,16 +398,33 @@ class _HomeScreenState extends State<HomeScreen> {
Map<String, dynamic> _calculateLeaders(List<Map<String, dynamic>> data) {
Map<String, int> ptsMap = {}; Map<String, int> astMap = {}; Map<String, int> rbsMap = {}; Map<String, String> namesMap = {};
for (var row in data) {
String pid = row['member_id'].toString();
String pid = row['member_id']?.toString() ?? "unknown";
namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido";
ptsMap[pid] = (ptsMap[pid] ?? 0) + (int.tryParse(row['pts']?.toString() ?? '0') ?? 0);
astMap[pid] = (astMap[pid] ?? 0) + (int.tryParse(row['ast']?.toString() ?? '0') ?? 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};
String getBest(Map<String, int> map) { var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key; return namesMap[bestId]!; }
int getBestVal(Map<String, int> map) => map.values.reduce((a, b) => a > b ? a : b);
return {'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap), 'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap), 'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)};
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) {
if (map.isEmpty) return '---';
var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key;
return namesMap[bestId] ?? '---';
}
int getBestVal(Map<String, int> map) {
if (map.isEmpty) return 0;
return map.values.reduce((a, b) => a > b ? a : b);
}
return {
'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap),
'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap),
'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)
};
}
Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) {

View File

@@ -1 +0,0 @@
import 'package:flutter/material.dart';