diff --git a/.metadata b/.metadata index 70a7d44..a7b35d5 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "9f455d2486bcb28cad87b062475f42edc959f636" + revision: "b45fa18946ecc2d9b4009952c636ba7e2ffbb787" channel: "stable" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 - base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 - - platform: android - create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 - base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 - - platform: ios - create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 - base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 - - platform: linux - create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 - base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 - - platform: macos - create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 - base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + create_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787 + base_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787 - platform: web - create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 - base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 - - platform: windows - create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 - base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + create_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787 + base_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787 # User provided section diff --git a/lib/controllers/placar_controller.dart b/lib/controllers/placar_controller.dart index b76713c..993d4c3 100644 --- a/lib/controllers/placar_controller.dart +++ b/lib/controllers/placar_controller.dart @@ -89,7 +89,6 @@ class PlacarController extends ChangeNotifier { double arcRadius = 0.459; double cornerY = 0.440; - // πŸ‘‡ NOVO: Temporizador para o Auto-Save nΓ£o travar a App Timer? _autoSaveTimer; Future loadPlayers() async { @@ -195,7 +194,7 @@ class PlacarController extends ChangeNotifier { "ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0, "p2m": s['p2m'] ?? 0, "p2a": s['p2a'] ?? 0, "p3m": s['p3m'] ?? 0, "p3a": s['p3a'] ?? 0, "so": s['so'] ?? 0, "il": s['il'] ?? 0, "li": s['li'] ?? 0, - "pa": s['pa'] ?? 0, "tres_s": s['tres_seg'] ?? 0, "dr": s['dr'] ?? 0, + "pa": s['pa'] ?? 0, "tres_seg": s['tres_seg'] ?? 0, "dr": s['dr'] ?? 0, "min": (s['minutos_jogados'] ?? 0) ~/ 60, "sec": s['minutos_jogados'] ?? 0, }; @@ -211,7 +210,7 @@ class PlacarController extends ChangeNotifier { "pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0, "ftm": 0, "fta": 0, "orb": 0, "drb": 0, "p2m": 0, "p2a": 0, "p3m": 0, "p3a": 0, - "so": 0, "il": 0, "li": 0, "pa": 0, "tres_s": 0, "dr": 0, + "so": 0, "il": 0, "li": 0, "pa": 0, "tres_seg": 0, "dr": 0, "min": 0, "sec": 0 }; @@ -228,7 +227,6 @@ class PlacarController extends ChangeNotifier { } } - // πŸ‘‡ MÁGICA 1: O "Anti-Spam". Ele acumula as mudanΓ§as e sΓ³ grava 1.5s depois de parares de clicar! void _scheduleAutoSave() { _autoSaveTimer?.cancel(); _autoSaveTimer = Timer(const Duration(milliseconds: 1500), () { @@ -304,7 +302,6 @@ class PlacarController extends ChangeNotifier { addTimeToCourt(myCourt); addTimeToCourt(oppCourt); - // Avisa APENAS o relΓ³gio (e nΓ£o a App inteira) durationNotifier.value -= const Duration(seconds: 1); } else { @@ -324,6 +321,7 @@ class PlacarController extends ChangeNotifier { isRunning = !isRunning; notifyListeners(); } + void useTimeout(bool isOpponent) { if (isOpponent) { if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++; @@ -485,169 +483,187 @@ class PlacarController extends ChangeNotifier { void commitStat(String action, String playerData) { bool isOpponent = playerData.startsWith("player_opp_"); - String playerId = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); + String playerId = playerData + .replaceAll("player_my_", "") + .replaceAll("player_opp_", ""); final stats = playerStats[playerId]!; - final name = playerNames[playerId] ?? "Jogador"; + final name = playerNames[playerId] ?? "Jogador"; - String logText = ""; + String logText = ""; + // ── PONTOS ────────────────────────────────────────────────────────────── if (action.startsWith("add_pts_")) { int pts = int.parse(action.split("_").last); - if (isOpponent) opponentScore += pts; else myScore += pts; + if (isOpponent) + opponentScore += pts; + else + myScore += pts; stats["pts"] = stats["pts"]! + pts; - if (pts == 2) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; stats["p2m"] = stats["p2m"]! + 1; stats["p2a"] = stats["p2a"]! + 1; } - if (pts == 3) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; stats["p3m"] = stats["p3m"]! + 1; stats["p3a"] = stats["p3a"]! + 1; } - if (pts == 1) { stats["ftm"] = stats["ftm"]! + 1; stats["fta"] = stats["fta"]! + 1; } + if (pts == 2) { + stats["fgm"] = stats["fgm"]! + 1; + stats["fga"] = stats["fga"]! + 1; + stats["p2m"] = stats["p2m"]! + 1; + stats["p2a"] = stats["p2a"]! + 1; + } + if (pts == 3) { + stats["fgm"] = stats["fgm"]! + 1; + stats["fga"] = stats["fga"]! + 1; + stats["p3m"] = stats["p3m"]! + 1; + stats["p3a"] = stats["p3a"]! + 1; + } + if (pts == 1) { + stats["ftm"] = stats["ftm"]! + 1; + stats["fta"] = stats["fta"]! + 1; + } logText = "marcou $pts pontos πŸ€"; - } + } + + // ── ANULAR PONTOS ──────────────────────────────────────────────────────── else if (action.startsWith("sub_pts_")) { int ptsToAnul = int.parse(action.split("_").last); - - int lastShotIndex = matchShots.lastIndexWhere((s) => - s.playerId == playerId && - s.isMake == true && - s.points == ptsToAnul - ); + + int lastShotIndex = matchShots.lastIndexWhere((s) => + s.playerId == playerId && s.isMake == true && s.points == ptsToAnul); if (lastShotIndex != -1) { matchShots.removeAt(lastShotIndex); - if (isOpponent) opponentScore -= ptsToAnul; else myScore -= ptsToAnul; + if (isOpponent) + opponentScore -= ptsToAnul; + else + myScore -= ptsToAnul; stats["pts"] = stats["pts"]! - ptsToAnul; - + if (ptsToAnul == 2) { - if(stats["p2m"]! > 0) stats["p2m"] = stats["p2m"]! - 1; - if(stats["p2a"]! > 0) stats["p2a"] = stats["p2a"]! - 1; - if(stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1; - if(stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1; + if (stats["p2m"]! > 0) stats["p2m"] = stats["p2m"]! - 1; + if (stats["p2a"]! > 0) stats["p2a"] = stats["p2a"]! - 1; + if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1; + if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1; } else if (ptsToAnul == 3) { - if(stats["p3m"]! > 0) stats["p3m"] = stats["p3m"]! - 1; - if(stats["p3a"]! > 0) stats["p3a"] = stats["p3a"]! - 1; - if(stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1; - if(stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1; + if (stats["p3m"]! > 0) stats["p3m"] = stats["p3m"]! - 1; + if (stats["p3a"]! > 0) stats["p3a"] = stats["p3a"]! - 1; + if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1; + if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1; } else if (ptsToAnul == 1) { - if(stats["ftm"]! > 0) stats["ftm"] = stats["ftm"]! - 1; - if(stats["fta"]! > 0) stats["fta"] = stats["fta"]! - 1; + if (stats["ftm"]! > 0) stats["ftm"] = stats["ftm"]! - 1; + if (stats["fta"]! > 0) stats["fta"] = stats["fta"]! - 1; } logText = "anulou cesto de $ptsToAnul pts βͺ"; } else { - return; + return; } } - else if (action == "miss_1") { stats["fta"] = stats["fta"]! + 1; logText = "falhou lance livre ❌"; } - else if (action == "miss_2") { stats["fga"] = stats["fga"]! + 1; stats["p2a"] = stats["p2a"]! + 1; logText = "falhou lanΓ§amento de 2 ❌"; } - else if (action == "miss_3") { stats["fga"] = stats["fga"]! + 1; stats["p3a"] = stats["p3a"]! + 1; logText = "falhou lanΓ§amento de 3 ❌"; } - 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_stl") { stats["stl"] = stats["stl"]! + 1; logText = "roubou a bola πŸ₯·"; } - else if (action == "add_blk") { stats["blk"] = stats["blk"]! + 1; logText = "fez um desarme (bloco) βœ‹"; } - - else if (action == "add_so") { stats["so"] = stats["so"]! + 1; logText = "sofreu uma falta πŸ€•"; } - else if (action == "add_il") { stats["il"] = stats["il"]! + 1; logText = "intercetou um lanΓ§amento πŸ›‘"; } - else if (action == "add_li") { stats["li"] = stats["li"]! + 1; logText = "teve o lanΓ§amento intercetado 🚫"; } - - else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; logText = "fez um passe ruim 🀦"; } - else if (action == "add_pa") { stats["pa"] = stats["pa"]! + 1; stats["tov"] = stats["tov"]! + 1; logText = "cometeu passos 🚢"; } - else if (action == "add_3s") { stats["tres_s"] = stats["tres_s"]! + 1; stats["tov"] = stats["tov"]! + 1; logText = "violaΓ§Γ£o de 3 segundos ⏱️"; } - else if (action == "add_24s") { stats["tov"] = stats["tov"]! + 1; logText = "violaΓ§Γ£o de 24 segundos ⏱️"; } - else if (action == "add_dr") { stats["dr"] = stats["dr"]! + 1; stats["tov"] = stats["tov"]! + 1; logText = "fez drible duplo πŸ€"; } - + + // ── FALHAS ─────────────────────────────────────────────────────────────── + else if (action == "miss_1") { + stats["fta"] = stats["fta"]! + 1; + logText = "falhou lance livre ❌"; + } else if (action == "miss_2") { + stats["fga"] = stats["fga"]! + 1; + stats["p2a"] = stats["p2a"]! + 1; + logText = "falhou lanΓ§amento de 2 ❌"; + } else if (action == "miss_3") { + stats["fga"] = stats["fga"]! + 1; + stats["p3a"] = stats["p3a"]! + 1; + logText = "falhou lanΓ§amento de 3 ❌"; + } + + // ── RESSALTOS ───────────────────────────────────────────────────────────── + 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 πŸ›‘οΈ"; + } + + // ── ASSISTÊNCIA ─────────────────────────────────────────────────────────── + else if (action == "add_ast") { + stats["ast"] = stats["ast"]! + 1; + logText = "fez uma assistΓͺncia 🀝"; + } + + // ── SOFRIDAS ────────────────────────────────────────────────────────────── + else if (action == "add_so") { + stats["so"] = stats["so"]! + 1; + logText = "sofreu uma falta πŸ€•"; + } + + // ══════════════════════════════════════════════════════════════════════════ + // STEAL β€” ROUBO DE BOLA + // ══════════════════════════════════════════════════════════════════════════ + else if (action == "add_stl" || action == "stl_steal") { + stats["stl"] = stats["stl"]! + 1; + logText = "roubou a bola πŸ₯·"; + } else if (action == "stl_intercept") { + stats["stl"] = stats["stl"]! + 1; + stats["il"] = stats["il"]! + 1; + logText = "intercetou um lanΓ§amento πŸ›‘"; + } + + // ══════════════════════════════════════════════════════════════════════════ + // BLOCK β€” DESARME + // ══════════════════════════════════════════════════════════════════════════ + else if (action == "add_blk" || action == "blk_made") { + stats["blk"] = stats["blk"]! + 1; + logText = "fez um desarme (bloco) βœ‹"; + } else if (action == "blk_suffered") { + stats["li"] = stats["li"]! + 1; + logText = "sofreu um desarme 🚫"; + } + + // AΓ§Γ΅es independentes legadas + else if (action == "add_il") { + stats["il"] = stats["il"]! + 1; + logText = "intercetou um lanΓ§amento πŸ›‘"; + } else if (action == "add_li") { + stats["li"] = stats["li"]! + 1; + logText = "teve o lanΓ§amento intercetado 🚫"; + } + + // ══════════════════════════════════════════════════════════════════════════ + // TURNOVER β€” PERDE DE BOLA E INFRAÇÕES + // ══════════════════════════════════════════════════════════════════════════ + else if (action == "add_tov" || action == "tov_badpass") { + stats["tov"] = stats["tov"]! + 1; + logText = "fez um passe ruim 🀦"; + } else if (action == "tov_3s") { + stats["tres_seg"] = stats["tres_seg"]! + 1; // SOMA AOS 3 SEGUNDOS + stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL + logText = "violaΓ§Γ£o de 3 segundos ⏱️"; + } else if (action == "tov_clock") { + stats["tov"] = stats["tov"]! + 1; + logText = "violaΓ§Γ£o de 24 segundos ⏱️"; + } else if (action == "tov_travel") { + stats["pa"] = stats["pa"]! + 1; // SOMA AOS PASSOS + stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL + logText = "cometeu passos 🚢"; + } else if (action == "tov_double") { + stats["dr"] = stats["dr"]! + 1; // SOMA AOS DRIBLES DUPLOS + stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL + logText = "fez drible duplo πŸ€"; + } + + // ── ANULAR FALTA ────────────────────────────────────────────────────────── else if (action == "sub_foul") { if (stats["fls"]! > 0) stats["fls"] = stats["fls"]! - 1; - if (isOpponent) { if (opponentFouls > 0) opponentFouls--; } else { if (myFouls > 0) myFouls--; } + if (isOpponent) { + if (opponentFouls > 0) opponentFouls--; + } else { + if (myFouls > 0) myFouls--; + } logText = "teve falta anulada πŸ”„"; } if (logText.isNotEmpty) { - String time = "${durationNotifier.value.inMinutes.toString().padLeft(2, '0')}:${durationNotifier.value.inSeconds.remainder(60).toString().padLeft(2, '0')}"; + String time = + "${durationNotifier.value.inMinutes.toString().padLeft(2, '0')}:${durationNotifier.value.inSeconds.remainder(60).toString().padLeft(2, '0')}"; playByPlay.insert(0, "P$currentQuarter - $time: $name $logText"); } - _scheduleAutoSave(); - notifyListeners(); - } - - Future saveGameStats(BuildContext context) async { - final supabase = Supabase.instance.client; - isSaving = true; + _scheduleAutoSave(); notifyListeners(); - - try { - bool isGameFinishedNow = currentQuarter >= 4 && durationNotifier.value.inSeconds == 0; - String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado'; - - String topPtsName = '---'; int maxPts = -1; - String topAstName = '---'; int maxAst = -1; - String topRbsName = '---'; int maxRbs = -1; - String mvpName = '---'; double maxMvpScore = -999.0; - - playerStats.forEach((playerId, stats) { - int pts = stats['pts'] ?? 0; - int ast = stats['ast'] ?? 0; - int rbs = stats['rbs'] ?? 0; - - double minJogados = (stats['sec'] ?? 0) / 60.0; - if (minJogados <= 0) minJogados = 40.0; - - int tr = rbs; - int br = stats['stl'] ?? 0; - int bp = stats['tov'] ?? 0; - int lFalhados = (stats['fga'] ?? 0) - (stats['fgm'] ?? 0); - int llFalhados = (stats['fta'] ?? 0) - (stats['ftm'] ?? 0); - - double mvpScore = ((pts * 0.30) + (tr * 0.20) + (ast * 0.35) + (br * 0.15)) - - ((bp * 0.35) + (lFalhados * 0.30) + (llFalhados * 0.35)); - mvpScore = mvpScore * (minJogados / 40.0); - - String pName = playerNames[playerId] ?? '---'; - - if (pts > maxPts && pts > 0) { maxPts = pts; topPtsName = '$pName ($pts)'; } - if (ast > maxAst && ast > 0) { maxAst = ast; topAstName = '$pName ($ast)'; } - if (rbs > maxRbs && rbs > 0) { maxRbs = rbs; topRbsName = '$pName ($rbs)'; } - if (mvpScore > maxMvpScore) { maxMvpScore = mvpScore; mvpName = '$pName (${mvpScore.toStringAsFixed(1)})'; } - }); - - await supabase.from('games').update({ - 'my_score': myScore, 'opponent_score': opponentScore, - 'remaining_seconds': durationNotifier.value.inSeconds, - 'my_timeouts': myTimeoutsUsed, 'opp_timeouts': opponentTimeoutsUsed, - 'current_quarter': currentQuarter, 'status': newStatus, - 'top_pts_name': topPtsName, 'top_ast_name': topAstName, - 'top_rbs_name': topRbsName, 'mvp_name': mvpName, - 'play_by_play': playByPlay, - }).eq('id', gameId); - - List> batchStats = []; - playerStats.forEach((playerId, stats) { - if (!playerId.startsWith("fake_")) { - bool isMyTeamPlayer = myCourt.contains(playerId) || myBench.contains(playerId); - batchStats.add({ - 'game_id': gameId, 'member_id': playerId, 'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!, - 'pts': stats['pts'], 'rbs': stats['rbs'], 'ast': stats['ast'], 'stl': stats['stl'], 'blk': stats['blk'], - 'tov': stats['tov'], 'fls': stats['fls'], 'fgm': stats['fgm'], 'fga': stats['fga'], 'ftm': stats['ftm'], - 'fta': stats['fta'], 'orb': stats['orb'], 'drb': stats['drb'], 'p2m': stats['p2m'], 'p2a': stats['p2a'], - 'p3m': stats['p3m'], 'p3a': stats['p3a'], - 'so': stats['so'], 'il': stats['il'], 'li': stats['li'], 'pa': stats['pa'], 'tres_seg': stats['tres_s'], - 'dr': stats['dr'], 'minutos_jogados': stats['sec'], - }); - } - }); - - await supabase.from('player_stats').delete().eq('game_id', gameId); - if (batchStats.isNotEmpty) await supabase.from('player_stats').insert(batchStats); - - final prefs = await SharedPreferences.getInstance(); - await prefs.remove('backup_$gameId'); - - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Guardado com Sucesso!'), backgroundColor: Colors.green)); - - } catch (e) { - debugPrint("Erro ao gravar estatΓ­sticas: $e"); - if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao guardar: $e'), backgroundColor: Colors.red)); - } finally { - isSaving = false; - notifyListeners(); - } } @override @@ -656,4 +672,140 @@ class PlacarController extends ChangeNotifier { _autoSaveTimer?.cancel(); super.dispose(); } + + Future saveGameStats(BuildContext context) async { + final supabase = Supabase.instance.client; + isSaving = true; + notifyListeners(); + + try { + bool isGameFinishedNow = + currentQuarter >= 4 && durationNotifier.value.inSeconds == 0; + String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado'; + + String topPtsName = '---'; + int maxPts = -1; + String topAstName = '---'; + int maxAst = -1; + String topRbsName = '---'; + int maxRbs = -1; + String mvpName = '---'; + double maxMvpScore = -999.0; + + playerStats.forEach((playerId, stats) { + int pts = stats['pts'] ?? 0; + int ast = stats['ast'] ?? 0; + int rbs = stats['rbs'] ?? 0; + + double minJogados = (stats['sec'] ?? 0) / 60.0; + if (minJogados <= 0) minJogados = 40.0; + + int tr = rbs; + int br = stats['stl'] ?? 0; + int bp = stats['tov'] ?? 0; + int lFalhados = (stats['fga'] ?? 0) - (stats['fgm'] ?? 0); + int llFalhados = (stats['fta'] ?? 0) - (stats['ftm'] ?? 0); + + double mvpScore = + ((pts * 0.30) + (tr * 0.20) + (ast * 0.35) + (br * 0.15)) - + ((bp * 0.35) + (lFalhados * 0.30) + (llFalhados * 0.35)); + mvpScore = mvpScore * (minJogados / 40.0); + + String pName = playerNames[playerId] ?? '---'; + + if (pts > maxPts && pts > 0) { + maxPts = pts; + topPtsName = '$pName ($pts)'; + } + if (ast > maxAst && ast > 0) { + maxAst = ast; + topAstName = '$pName ($ast)'; + } + if (rbs > maxRbs && rbs > 0) { + maxRbs = rbs; + topRbsName = '$pName ($rbs)'; + } + if (mvpScore > maxMvpScore) { + maxMvpScore = mvpScore; + mvpName = '$pName (${mvpScore.toStringAsFixed(1)})'; + } + }); + + await supabase.from('games').update({ + 'my_score': myScore, + 'opponent_score': opponentScore, + 'remaining_seconds': durationNotifier.value.inSeconds, + 'my_timeouts': myTimeoutsUsed, + 'opp_timeouts': opponentTimeoutsUsed, + 'current_quarter': currentQuarter, + 'status': newStatus, + 'top_pts_name': topPtsName, + 'top_ast_name': topAstName, + 'top_rbs_name': topRbsName, + 'mvp_name': mvpName, + 'play_by_play': playByPlay, + }).eq('id', gameId); + + List> batchStats = []; + playerStats.forEach((playerId, stats) { + if (!playerId.startsWith("fake_")) { + bool isMyTeamPlayer = + myCourt.contains(playerId) || myBench.contains(playerId); + batchStats.add({ + 'game_id': gameId, + 'member_id': playerId, + 'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!, + 'pts': stats['pts'], + 'rbs': stats['rbs'], + 'ast': stats['ast'], + 'stl': stats['stl'], + 'blk': stats['blk'], + 'tov': stats['tov'], + 'fls': stats['fls'], + 'fgm': stats['fgm'], + 'fga': stats['fga'], + 'ftm': stats['ftm'], + 'fta': stats['fta'], + 'orb': stats['orb'], + 'drb': stats['drb'], + 'p2m': stats['p2m'], + 'p2a': stats['p2a'], + 'p3m': stats['p3m'], + 'p3a': stats['p3a'], + 'so': stats['so'], // As Faltas Sofridas + 'il': stats['il'], + 'li': stats['li'], + 'pa': stats['pa'], + 'tres_seg': stats['tres_seg'], // Os 3 Segundos com o nome correto + 'dr': stats['dr'], + 'minutos_jogados': stats['sec'], + }); + } + }); + + await supabase.from('player_stats').delete().eq('game_id', gameId); + if (batchStats.isNotEmpty) { + await supabase.from('player_stats').insert(batchStats); + } + + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('backup_$gameId'); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Guardado com Sucesso!'), + backgroundColor: Colors.green)); + } + } catch (e) { + debugPrint("Erro ao gravar estatΓ­sticas: $e"); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Erro ao guardar: $e'), + backgroundColor: Colors.red)); + } + } finally { + isSaving = false; + notifyListeners(); + } + } } \ No newline at end of file diff --git a/lib/pages/PlacarPage.dart b/lib/pages/PlacarPage.dart index 25c47ec..3a4ed8a 100644 --- a/lib/pages/PlacarPage.dart +++ b/lib/pages/PlacarPage.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:playmaker/icons.dart/resaltosicon.dart'; -import 'package:playmaker/widgets/placar_widgets.dart'; +import 'package:playmaker/widgets/placar_widgets.dart'; // MantΓ©m este import import 'dart:math' as math; - -import '../utils/size_extension.dart'; import '../classe/theme.dart'; import '../controllers/placar_controller.dart'; import 'package:playmaker/zone_map_dialog.dart'; @@ -152,7 +150,6 @@ class _PlacarPageState extends State { builder: (context, constraints) { final w = constraints.maxWidth; final h = constraints.maxHeight; - return Stack( children: [ GestureDetector( @@ -179,7 +176,6 @@ class _PlacarPageState extends State { ), ), ), - if (!_controller.isSelectingShotLocation && _controller.myCourt.length >= 5 && _controller.oppCourt.length >= 5) ...[ Positioned(top: h * 0.25, left: w * 0.02, child: PlayerCourtCard(controller: _controller, playerId: _controller.myCourt[0], isOpponent: false, sf: sf)), Positioned(top: h * 0.68, left: w * 0.02, child: PlayerCourtCard(controller: _controller, playerId: _controller.myCourt[1], isOpponent: false, sf: sf)), @@ -193,12 +189,10 @@ class _PlacarPageState extends State { Positioned(top: h * 0.15, right: w * 0.20, child: PlayerCourtCard(controller: _controller, playerId: _controller.oppCourt[3], isOpponent: true, sf: sf)), Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, playerId: _controller.oppCourt[4], isOpponent: true, sf: sf)), ], - if (!_controller.isSelectingShotLocation) ...[ _buildFloatingFoulBtn("FALTA +", AppTheme.actionPoints, "add_foul", Icons.sports, w * 0.39, 0.0, h * 0.31, sf), _buildFloatingFoulBtn("FALTA -", AppTheme.actionMiss, "sub_foul", Icons.block, 0.0, w * 0.39, h * 0.31, sf), ], - if (!_controller.isSelectingShotLocation) Positioned( top: (h * 0.32) + (40 * sf), @@ -214,11 +208,9 @@ class _PlacarPageState extends State { ), ), ), - Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))), if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller, sf: sf)), - if (_controller.isSelectingShotLocation) Positioned( top: h * 0.4, left: 0, right: 0, @@ -253,22 +245,31 @@ class _PlacarPageState extends State { children: [ _buildCornerBtn(heroTag: 'btn_heatmap', icon: Icons.local_fire_department, color: Colors.orange.shade800, size: cornerBtnSize, onTap: () => _showHeatmap(context)), SizedBox(height: 10 * sf), - _buildCornerBtn(heroTag: 'btn_boxscore', icon: Icons.table_chart, color: Colors.indigo, size: cornerBtnSize, onTap: () => showDialog(context: context, builder: (ctx) => BoxScoreDialog(controller: _controller, sf: sf))), ], + _buildCornerBtn(heroTag: 'btn_boxscore', icon: Icons.table_chart, color: Colors.indigo, size: cornerBtnSize, onTap: () => showDialog(context: context, builder: (ctx) => BoxScoreDialog(controller: _controller, sf: sf))), + ], ), ), - if (_controller.showMyBench) - Positioned(bottom: 180 * sf, left: 12 * sf, child: BenchPopup(controller: _controller, isOpponent: false, sf: sf)), - - if (_controller.showOppBench) - Positioned(bottom: 180 * sf, right: 12 * sf, child: BenchPopup(controller: _controller, isOpponent: true, sf: sf)), - + // BOTΓ•ES INFERIORES: SUBSTITUIÇÕES E TIMEOUTS Positioned( bottom: 55 * sf, left: 12 * sf, child: Column( mainAxisSize: MainAxisSize.min, children: [ - _buildCornerBtn(heroTag: 'btn_sub_home', icon: Icons.swap_horiz, color: AppTheme.myTeamBlue, size: cornerBtnSize, onTap: () { _controller.showMyBench = !_controller.showMyBench; _controller.showOppBench = false; _controller.notifyListeners(); }), + _buildCornerBtn( + heroTag: 'btn_sub_home', + icon: Icons.swap_horiz, + color: AppTheme.myTeamBlue, + size: cornerBtnSize, + onTap: () => showDialog( + context: context, + builder: (ctx) => SubstitutionDialog( + controller: _controller, + isOpponent: false, + sf: sf, + ), + ), + ), SizedBox(height: 12 * sf), _buildCornerBtn(heroTag: 'btn_to_home', icon: Icons.timer, color: AppTheme.myTeamBlue, size: cornerBtnSize, onTap: _controller.myTimeoutsUsed >= 3 ? null : () => _controller.useTimeout(false)), ], @@ -280,7 +281,20 @@ class _PlacarPageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - _buildCornerBtn(heroTag: 'btn_sub_away', icon: Icons.swap_horiz, color: AppTheme.oppTeamRed, size: cornerBtnSize, onTap: () { _controller.showOppBench = !_controller.showOppBench; _controller.showMyBench = false; _controller.notifyListeners(); }), + _buildCornerBtn( + heroTag: 'btn_sub_away', + icon: Icons.swap_horiz, + color: AppTheme.oppTeamRed, + size: cornerBtnSize, + onTap: () => showDialog( + context: context, + builder: (ctx) => SubstitutionDialog( + controller: _controller, + isOpponent: true, + sf: sf, + ), + ), + ), SizedBox(height: 12 * sf), _buildCornerBtn(heroTag: 'btn_to_away', icon: Icons.timer, color: AppTheme.oppTeamRed, size: cornerBtnSize, onTap: _controller.opponentTimeoutsUsed >= 3 ? null : () => _controller.useTimeout(true)), ], @@ -297,1094 +311,4 @@ class _PlacarPageState extends State { }, ); } -} - -// ============================================================================== -// WIDGETS COMPONENTIZADOS E POP-UPS -// ============================================================================== - -class ActionSubtypeDialog extends StatelessWidget { - final String title; - final Map options; - final Function(String) onSelected; - final double sf; - - const ActionSubtypeDialog({super.key, required this.title, required this.options, required this.onSelected, required this.sf}); - - @override - Widget build(BuildContext context) { - return Dialog( - backgroundColor: Colors.transparent, - elevation: 0, - child: Container( - width: MediaQuery.of(context).size.width * 0.55, - decoration: BoxDecoration( - color: AppTheme.placarDarkSurface, - borderRadius: BorderRadius.circular(12 * sf), - border: Border.all(color: AppTheme.warningAmber, width: 1.5 * sf), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 12 * sf), - decoration: BoxDecoration( - color: AppTheme.placarListCard, - borderRadius: BorderRadius.vertical(top: Radius.circular(10 * sf)), - ), - child: Stack( - alignment: Alignment.center, - children: [ - Align( - alignment: Alignment.center, - child: Text( - title, - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf), - ), - ), - Align( - alignment: Alignment.centerRight, - child: InkWell( - onTap: () => Navigator.pop(context), - child: Container( - padding: EdgeInsets.all(4 * sf), - decoration: const BoxDecoration(color: Colors.white24, shape: BoxShape.circle), - child: Icon(Icons.close, color: Colors.white, size: 16 * sf), - ), - ), - ), - ], - ), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 20 * sf, horizontal: 15 * sf), - child: Wrap( - spacing: 12 * sf, - runSpacing: 15 * sf, - alignment: WrapAlignment.center, - children: options.entries.map((e) => SizedBox( - width: 110 * sf, - height: 60 * sf, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.placarTimerBg, - foregroundColor: Colors.white, - padding: EdgeInsets.all(6 * sf), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8 * sf), - side: BorderSide(color: Colors.white12, width: 1 * sf), - ), - ), - onPressed: () => onSelected(e.key), - child: Text( - e.value, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12 * sf), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - )).toList(), - ), - ), - ], - ), - ), - ); - } -} - -void showFoulVictimDialog(BuildContext context, PlacarController controller, bool isCommitterOpponent, String committerId, String foulType, double sf) { - final victimCourt = isCommitterOpponent ? controller.myCourt : controller.oppCourt; - - final prefixCommitter = isCommitterOpponent ? "player_opp_" : "player_my_"; - final prefixVictim = isCommitterOpponent ? "player_my_" : "player_opp_"; - - final victimsColor = isCommitterOpponent ? AppTheme.myTeamBlue : AppTheme.oppTeamRed; - - final possibleVictims = victimCourt.where((id) => !id.startsWith("fake_")).toList(); - - showDialog( - context: context, - barrierDismissible: false, - builder: (ctx) => Dialog( - backgroundColor: Colors.transparent, - elevation: 0, - child: Container( - width: MediaQuery.of(context).size.width * 0.60, - decoration: BoxDecoration( - color: AppTheme.placarDarkSurface, - borderRadius: BorderRadius.circular(12 * sf), - border: Border.all(color: AppTheme.warningAmber, width: 1.5 * sf), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 12 * sf), - decoration: BoxDecoration( - color: AppTheme.placarListCard, - borderRadius: BorderRadius.vertical(top: Radius.circular(10 * sf)), - ), - child: Stack( - alignment: Alignment.center, - children: [ - Align( - alignment: Alignment.center, - child: Text( - "Quem sofreu a falta?", - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf), - ), - ), - Align( - alignment: Alignment.centerRight, - child: InkWell( - onTap: () => Navigator.pop(ctx), - child: Container( - padding: EdgeInsets.all(4 * sf), - decoration: const BoxDecoration(color: Colors.white24, shape: BoxShape.circle), - child: Icon(Icons.close, color: Colors.white, size: 16 * sf), - ), - ), - ), - ], - ), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 20 * sf, horizontal: 10 * sf), - child: Column( - children: [ - Wrap( - spacing: 10 * sf, - runSpacing: 10 * sf, - alignment: WrapAlignment.center, - children: possibleVictims.map((id) { - final name = controller.playerNames[id] ?? "Desconhecido"; - final number = controller.playerNumbers[id] ?? "0"; - - return InkWell( - onTap: () { - Navigator.pop(ctx); - controller.registerFoul("$prefixCommitter$committerId", foulType, "$prefixVictim$id"); - }, - child: Container( - width: 80 * sf, - padding: EdgeInsets.all(6 * sf), - decoration: BoxDecoration( - color: victimsColor.withOpacity(0.2), - border: Border.all(color: victimsColor, width: 1.5 * sf), - borderRadius: BorderRadius.circular(12 * sf), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircleAvatar( - backgroundColor: victimsColor, - radius: 16 * sf, - child: Text(number, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * sf)), - ), - SizedBox(height: 6 * sf), - Text(name, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 10 * sf), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis), - ], - ), - ), - ); - }).toList(), - ), - SizedBox(height: 15 * sf), - const Divider(color: Colors.white24), - SizedBox(height: 8 * sf), - ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.placarTimerBg, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 10 * sf), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8 * sf), - side: BorderSide(color: Colors.white12, width: 1 * sf), - ), - ), - icon: Icon(Icons.group, color: Colors.white, size: 16 * sf), - label: Text("Equipa / Sem VΓ­tima EspecΓ­fica", style: TextStyle(fontSize: 12 * sf)), - onPressed: () { - Navigator.pop(ctx); - controller.registerFoul("$prefixCommitter$committerId", foulType, ""); - }, - ) - ], - ), - ), - ], - ), - ), - ), - ); -} - -void showAssistDialog(BuildContext context, PlacarController controller, bool isOpponent, String scorerId, double sf) { - final teamCourt = isOpponent ? controller.oppCourt : controller.myCourt; - final prefix = isOpponent ? "player_opp_" : "player_my_"; - final teamColor = isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue; - - final possibleAssistants = teamCourt.where((id) => id != scorerId && !id.startsWith("fake_")).toList(); - - if (possibleAssistants.isEmpty) return; - - showDialog( - context: context, - barrierDismissible: false, - builder: (ctx) => Dialog( - backgroundColor: Colors.transparent, - elevation: 0, - child: Container( - width: MediaQuery.of(context).size.width * 0.55, - decoration: BoxDecoration( - color: AppTheme.placarDarkSurface, - borderRadius: BorderRadius.circular(12 * sf), - border: Border.all(color: AppTheme.warningAmber, width: 1.5 * sf), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 12 * sf), - decoration: BoxDecoration( - color: AppTheme.placarListCard, - borderRadius: BorderRadius.vertical(top: Radius.circular(10 * sf)), - ), - child: Stack( - alignment: Alignment.center, - children: [ - Align( - alignment: Alignment.center, - child: Text( - "Houve AssistΓͺncia?", - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf), - ), - ), - Align( - alignment: Alignment.centerRight, - child: InkWell( - onTap: () => Navigator.pop(ctx), - child: Container( - padding: EdgeInsets.all(4 * sf), - decoration: const BoxDecoration(color: Colors.white24, shape: BoxShape.circle), - child: Icon(Icons.close, color: Colors.white, size: 16 * sf), - ), - ), - ), - ], - ), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 20 * sf, horizontal: 10 * sf), - child: Column( - children: [ - Wrap( - spacing: 10 * sf, - runSpacing: 10 * sf, - alignment: WrapAlignment.center, - children: possibleAssistants.map((id) { - final name = controller.playerNames[id] ?? "Desconhecido"; - final number = controller.playerNumbers[id] ?? "0"; - - return InkWell( - onTap: () { - Navigator.pop(ctx); - controller.commitStat("add_ast", "$prefix$id"); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('AssistΓͺncia: $name'), duration: const Duration(seconds: 1), backgroundColor: AppTheme.successGreen)); - }, - child: Container( - width: 75 * sf, - padding: EdgeInsets.all(6 * sf), - decoration: BoxDecoration( - color: teamColor.withOpacity(0.2), - border: Border.all(color: teamColor, width: 1.5 * sf), - borderRadius: BorderRadius.circular(12 * sf), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircleAvatar( - backgroundColor: teamColor, - radius: 16 * sf, - child: Text(number, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * sf)), - ), - SizedBox(height: 6 * sf), - Text(name, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 10 * sf), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis), - ], - ), - ), - ); - }).toList(), - ), - SizedBox(height: 15 * sf), - const Divider(color: Colors.white24), - SizedBox(height: 8 * sf), - ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.placarTimerBg, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 10 * sf), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8 * sf), - side: BorderSide(color: Colors.white12, width: 1 * sf), - ), - ), - icon: Icon(Icons.person_off, color: Colors.white, size: 16 * sf), - label: Text("Isolado (Sem assistΓͺncia)", style: TextStyle(fontSize: 12 * sf)), - onPressed: () => Navigator.pop(ctx), - ) - ], - ), - ), - ], - ), - ), - ), - ); -} - -// ============================================================================ -// SHIRT PAINTER β€” Desenho 100% cΓ³digo (Formato Jersey Realista) -// ============================================================================ -class ShirtPainter extends CustomPainter { - final Color color; - final bool isFouledOut; - const ShirtPainter({required this.color, this.isFouledOut = false}); - - @override - void paint(Canvas canvas, Size size) { - final double w = size.width; - final double h = size.height; - final Color shirtColor = isFouledOut ? Colors.grey.shade700 : color; - - // Tinta para preencher a cor da camisola - final paint = Paint() - ..color = shirtColor - ..style = PaintingStyle.fill; - - // Tinta para fazer a borda branca (tipo o acabamento do tecido) - final trimPaint = Paint() - ..color = Colors.white - ..style = PaintingStyle.stroke - ..strokeWidth = w * 0.04 - ..strokeJoin = StrokeJoin.round; - - final path = Path(); - - // 1. Ombro esquerdo (lado do pescoΓ§o) - path.moveTo(w * 0.32, h * 0.10); - - // 2. Ombro esquerdo (lado do braΓ§o) - path.lineTo(w * 0.18, h * 0.10); - - // 3. Cava do braΓ§o esquerdo (curva funda) - path.quadraticBezierTo(w * 0.28, h * 0.35, w * 0.05, h * 0.55); - - // 4. Lado esquerdo (desce atΓ© baixo) - path.lineTo(w * 0.15, h * 1.1); - - // 5. Fundo da camisola (linha reta em baixo) - path.lineTo(w * 0.85, h * 1.1); - - // 6. Lado direito (sobe atΓ© Γ  axila) - path.lineTo(w * 0.95, h * 0.55); - - // 7. Cava do braΓ§o direito (curva funda) - path.quadraticBezierTo(w * 0.72, h * 0.35, w * 0.82, h * 0.10); - - // 8. Ombro direito (lado do braΓ§o atΓ© ao pescoΓ§o) - path.lineTo(w * 0.68, h * 0.10); - - // 9. Gola (decote redondo profundo) - path.quadraticBezierTo(w * 0.50, h * 0.45, w * 0.32, h * 0.10); - - path.close(); - - // Desenha o fundo da cor da equipa - canvas.drawPath(path, paint); - - // Desenha a borda branca por cima para dar estilo - canvas.drawPath(path, trimPaint); - } - - @override - bool shouldRepaint(ShirtPainter old) => old.color != color || old.isFouledOut != isFouledOut; -} -// ============================================================================ -// CARD DO JOGADOR NO CAMPO -// ============================================================================ -class PlayerCourtCard extends StatelessWidget { - final PlacarController controller; - final String playerId; - final bool isOpponent; - final double sf; - - const PlayerCourtCard({super.key, required this.controller, required this.playerId, required this.isOpponent, required this.sf}); - - @override - Widget build(BuildContext context) { - final teamColor = isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue; - - final realName = controller.playerNames[playerId] ?? "Erro"; - final stats = controller.playerStats[playerId]!; - final number = controller.playerNumbers[playerId]!; - final prefix = isOpponent ? "player_opp_" : "player_my_"; - - return Draggable( - data: "$prefix$playerId", - feedback: Material( - color: Colors.transparent, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(6)), - child: Text(realName, style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), - ), - ), - childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, realName, stats, teamColor, false, false, sf)), - child: DragTarget( - onAcceptWithDetails: (details) { - final action = details.data; - - if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") { - bool isMake = action.startsWith("add_"); - bool is3Pt = action.endsWith("_3"); - - showDialog( - context: context, - builder: (ctx) => ZoneMapDialog( - playerName: realName, - isMake: isMake, - is3PointAction: is3Pt, - onZoneSelected: (zone, points, relX, relY) { - Navigator.pop(ctx); - controller.registerShotFromPopup(context, action, "$prefix$playerId", zone, points, relX, relY); - - if (isMake) { - showAssistDialog(context, controller, isOpponent, playerId, sf); - } - }, - ), - ); - } - else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) { - controller.handleActionDrag(context, action, "$prefix$playerId"); - } - else if (action.startsWith("bench_")) { - controller.handleSubbing(context, action, playerId, isOpponent); - } - }, - builder: (context, candidateData, rejectedData) { - bool isSubbing = candidateData.any((data) => data != null && (data.startsWith("bench_my_") || data.startsWith("bench_opp_"))); - bool isActionHover = candidateData.any((data) => data != null && (data.startsWith("add_") || data.startsWith("sub_") || data.startsWith("miss_"))); - - return _playerCardUI(number, realName, stats, teamColor, isSubbing, isActionHover, sf); - }, - ), - ); - } - - Widget _playerCardUI(String number, String displayNameStr, Map stats, Color teamColor, bool isSubbing, bool isActionHover, double sf) { - bool isFouledOut = stats["fls"]! >= 5; - Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white; - Color borderColor = isFouledOut ? AppTheme.actionMiss : Colors.transparent; - - if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = AppTheme.myTeamBlue; } - else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = AppTheme.actionPoints; } - - int fgm = stats["fgm"]!; int fga = stats["fga"]!; - String fgPercent = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) : "0"; - String displayName = displayNameStr.length > 12 ? "${displayNameStr.substring(0, 10)}..." : displayNameStr; - - // Tamanho da camisola ajustado para ficar perfeito no cartΓ£o - final double shirtSize = 40 * sf; - - return Container( - padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 6 * sf), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(8 * sf), - border: Border.all(color: borderColor, width: 1.5 * sf), - boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4 * sf, offset: Offset(0, 2 * sf))], - ), - child: IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, // Centra verticalmente a camisola com o texto - children: [ - // ── APENAS A CAMISOLA (Sem quadrado de fundo) ── - SizedBox( - width: shirtSize, - height: shirtSize, - child: Stack( - alignment: Alignment.center, - children: [ - CustomPaint( - size: Size(shirtSize, shirtSize), - painter: ShirtPainter( - color: teamColor, - isFouledOut: isFouledOut, - ), - ), - Padding( - padding: EdgeInsets.only(top: shirtSize * 0.15), - child: Text( - number, - style: TextStyle( - color: Colors.white, - fontSize: shirtSize * 0.40, - fontWeight: FontWeight.w900, - decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none, - shadows: const [Shadow(color: Colors.black45, blurRadius: 2, offset: Offset(1, 1))], - ), - ), - ), - ], - ), - ), - SizedBox(width: 8 * sf), // EspaΓ§o entre a camisola e as estatΓ­sticas - - // ── EstatΓ­sticas ───────────────────────────────────── - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text(displayName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? AppTheme.actionMiss : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), - SizedBox(height: 1.5 * sf), - Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[700], fontWeight: FontWeight.w600)), - Text("${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls", style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[500], fontWeight: FontWeight.w600)), - ], - ), - ], - ), - ), - ); - } -} - -class HeatmapDialog extends StatefulWidget { - final List shots; - final String myTeamName; - final String oppTeamName; - final List myPlayersIds; - final List oppPlayersIds; - final Map> playerStats; - final Map playerNames; - - const HeatmapDialog({ - super.key, - required this.shots, - required this.myTeamName, - required this.oppTeamName, - required this.myPlayersIds, - required this.oppPlayersIds, - required this.playerStats, - required this.playerNames, - }); - - @override - State createState() => _HeatmapDialogState(); -} - -class _HeatmapDialogState extends State { - bool _isMapVisible = false; - String _selectedTeam = ''; - String _selectedPlayerId = ''; - - @override - Widget build(BuildContext context) { - final Color headerColor = const Color(0xFFE88F15); - final Color yellowBackground = const Color(0xFFDFAB00); - - final double screenHeight = MediaQuery.of(context).size.height; - final double dialogHeight = screenHeight * 0.95; - final double dialogWidth = dialogHeight * 1.0; - - return Dialog( - backgroundColor: yellowBackground, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - clipBehavior: Clip.antiAlias, - insetPadding: const EdgeInsets.all(10), - child: SizedBox( - height: dialogHeight, - width: dialogWidth, - child: _isMapVisible ? _buildMapScreen(headerColor) : _buildSelectionScreen(headerColor), - ), - ); - } - - Widget _buildSelectionScreen(Color headerColor) { - return Column( - children: [ - Container( - height: 40, - color: headerColor, - width: double.infinity, - child: Stack( - alignment: Alignment.center, - children: [ - const Text( - "ESCOLHE A EQUIPA OU UM JOGADOR", - style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold), - ), - Positioned( - right: 8, - child: InkWell( - onTap: () => Navigator.pop(context), - child: Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), - child: Icon(Icons.close, color: headerColor, size: 16), - ), - ), - ) - ], - ), - ), - - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: _buildTeamColumn( - teamName: widget.myTeamName, - playerIds: widget.myPlayersIds, - teamColor: AppTheme.myTeamBlue, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildTeamColumn( - teamName: widget.oppTeamName, - playerIds: widget.oppPlayersIds, - teamColor: AppTheme.oppTeamRed, - ), - ), - ], - ), - ), - ), - ], - ); - } - - Widget _buildTeamColumn({required String teamName, required List playerIds, required Color teamColor}) { - List realPlayerIds = playerIds.where((id) => !id.startsWith("fake_")).toList(); - - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - children: [ - InkWell( - onTap: () => setState(() { - _selectedTeam = teamName; - _selectedPlayerId = 'Todos'; - _isMapVisible = true; - }), - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 10), - decoration: BoxDecoration( - color: teamColor, - borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)), - ), - child: Column( - children: [ - Text(teamName.toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), - decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(12)), - child: const Text("MAPA GERAL DA EQUIPA", style: TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)), - ), - ], - ), - ), - ), - Expanded( - child: ListView.separated( - itemCount: realPlayerIds.length, - separatorBuilder: (context, index) => const Divider(height: 1, color: Colors.black12), - itemBuilder: (context, index) { - String pId = realPlayerIds[index]; - String pName = widget.playerNames[pId] ?? 'Desconhecido'; - int pts = widget.playerStats[pId]?['pts'] ?? 0; - - return ListTile( - dense: true, - visualDensity: VisualDensity.compact, - leading: Icon(Icons.person, color: teamColor), - title: Text(pName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)), - trailing: Text("$pts Pts", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: teamColor)), - onTap: () => setState(() { - _selectedTeam = teamName; - _selectedPlayerId = pId; - _isMapVisible = true; - }), - ); - }, - ), - ), - ], - ), - ); - } - - Widget _buildMapScreen(Color headerColor) { - List filteredShots = widget.shots.where((s) { - if (_selectedPlayerId != 'Todos') return s.playerId == _selectedPlayerId; - if (_selectedTeam == widget.myTeamName) return widget.myPlayersIds.contains(s.playerId); - if (_selectedTeam == widget.oppTeamName) return widget.oppPlayersIds.contains(s.playerId); - return true; - }).toList(); - - String titleText = _selectedPlayerId == 'Todos' - ? "MAPA GERAL: ${_selectedTeam.toUpperCase()}" - : "MAPA: ${widget.playerNames[_selectedPlayerId]?.toUpperCase() ?? ''}"; - - return Column( - children: [ - Container( - height: 40, - color: headerColor, - width: double.infinity, - child: Stack( - alignment: Alignment.center, - children: [ - Positioned( - left: 8, - child: InkWell( - onTap: () => setState(() => _isMapVisible = false), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), - child: Row( - children: [ - Icon(Icons.arrow_back, color: headerColor, size: 14), - const SizedBox(width: 4), - Text("VOLTAR", style: TextStyle(color: headerColor, fontWeight: FontWeight.bold, fontSize: 12)), - ], - ), - ), - ), - ), - Text( - titleText, - style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold), - ), - Positioned( - right: 8, - child: InkWell( - onTap: () => Navigator.pop(context), - child: Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), - child: Icon(Icons.close, color: headerColor, size: 16), - ), - ), - ) - ], - ), - ), - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - return Stack( - children: [ - CustomPaint( - size: Size(constraints.maxWidth, constraints.maxHeight), - painter: HeatmapCourtPainter(), - ), - ...filteredShots.map((shot) => Positioned( - left: (shot.relativeX * constraints.maxWidth) - 8, - top: (shot.relativeY * constraints.maxHeight) - 8, - child: CircleAvatar( - radius: 8, - backgroundColor: shot.isMake ? AppTheme.successGreen : AppTheme.actionMiss, - child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white) - ), - )), - ], - ); - }, - ), - ), - ], - ); - } -} - -class HeatmapCourtPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final double w = size.width; - final double h = size.height; - final double basketX = w / 2; - - final Paint whiteStroke = Paint()..color = Colors.white..style = PaintingStyle.stroke..strokeWidth = 2.0; - final Paint blackStroke = Paint()..color = Colors.black87..style = PaintingStyle.stroke..strokeWidth = 2.0; - - final double margin = w * 0.10; - final double length = h * 0.35; - final double larguraDoArco = (w / 2) - margin; - final double alturaDoArco = larguraDoArco * 0.30; - final double totalArcoHeight = alturaDoArco * 4; - - canvas.drawLine(Offset(margin, 0), Offset(margin, length), whiteStroke); - canvas.drawLine(Offset(w - margin, 0), Offset(w - margin, length), whiteStroke); - canvas.drawLine(Offset(0, length), Offset(margin, length), whiteStroke); - canvas.drawLine(Offset(w - margin, length), Offset(w, length), whiteStroke); - canvas.drawArc(Rect.fromCenter(center: Offset(basketX, length), width: larguraDoArco * 2, height: totalArcoHeight), 0, math.pi, false, whiteStroke); - - double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75)); - double sYL = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.75)); - double sXR = basketX + (larguraDoArco * math.cos(math.pi * 0.25)); - double sYR = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.25)); - - canvas.drawLine(Offset(sXL, sYL), Offset(0, h * 0.85), whiteStroke); - canvas.drawLine(Offset(sXR, sYR), Offset(w, h * 0.85), whiteStroke); - - final double pW = w * 0.28; - final double pH = h * 0.38; - canvas.drawRect(Rect.fromLTWH(basketX - pW / 2, 0, pW, pH), blackStroke); - - final double ftR = pW / 2; - canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), 0, math.pi, false, blackStroke); - for (int i = 0; i < 10; i++) { - canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), math.pi + (i * 2 * (math.pi / 20)), math.pi / 20, false, blackStroke); - } - - canvas.drawLine(Offset(basketX - pW / 2, pH), Offset(sXL, sYL), blackStroke); - canvas.drawLine(Offset(basketX + pW / 2, pH), Offset(sXR, sYR), blackStroke); - - canvas.drawArc(Rect.fromCircle(center: Offset(basketX, h), radius: w * 0.12), math.pi, math.pi, false, blackStroke); - canvas.drawCircle(Offset(basketX, h * 0.12), w * 0.02, blackStroke); - canvas.drawLine(Offset(basketX - w * 0.08, h * 0.12 - 5), Offset(basketX + w * 0.08, h * 0.12 - 5), blackStroke); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} - -// ============================================================================ -// 5. CAIXA DE HISTΓ“RICO (PLAY-BY-PLAY) -// ============================================================================ -class PlayByPlayDialog extends StatelessWidget { - final PlacarController controller; - const PlayByPlayDialog({super.key, required this.controller}); - - @override - Widget build(BuildContext context) { - return Dialog( - backgroundColor: AppTheme.placarDarkSurface, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Container( - width: 400, - height: MediaQuery.of(context).size.height * 0.8, - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text("HISTΓ“RICO DE JOGADAS", style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), - IconButton(icon: const Icon(Icons.close, color: Colors.white), onPressed: () => Navigator.pop(context)) - ], - ), - const Divider(color: Colors.white24), - Expanded( - child: controller.playByPlay.isEmpty - ? const Center(child: Text("Ainda nΓ£o hΓ‘ jogadas.", style: TextStyle(color: Colors.white54))) - : ListView.separated( - itemCount: controller.playByPlay.length, - separatorBuilder: (_, __) => const Divider(color: Colors.white10), - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Text( - controller.playByPlay[index], - style: const TextStyle(color: Colors.white, fontSize: 14), - ), - ); - }, - ), - ), - ], - ), - ), - ); - } -} - -// ============================================================================ -// 6. ECRΓƒ DE BOX SCORE (ESTATÍSTICAS GERAIS) -// ============================================================================ -class BoxScoreDialog extends StatelessWidget { - final PlacarController controller; - final double sf; - - const BoxScoreDialog({super.key, required this.controller, required this.sf}); - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: controller, - builder: (context, child) { - return Dialog( - backgroundColor: AppTheme.placarDarkSurface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12 * sf), - side: BorderSide(color: Colors.white24, width: 1 * sf), - ), - insetPadding: EdgeInsets.all(8 * sf), - clipBehavior: Clip.antiAlias, - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.98, - height: MediaQuery.of(context).size.height * 0.98, - child: DefaultTabController( - length: 2, - child: Column( - children: [ - Padding( - padding: EdgeInsets.fromLTRB(16 * sf, 16 * sf, 8 * sf, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("BOX SCORE", style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)), - IconButton( - icon: Icon(Icons.close, color: Colors.white, size: 24 * sf), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () => Navigator.pop(context) - ) - ], - ), - ), - SizedBox(height: 8 * sf), - - TabBar( - indicatorColor: AppTheme.warningAmber, - labelColor: Colors.white, - unselectedLabelColor: Colors.white54, - labelStyle: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold), - indicatorWeight: 3 * sf, - dividerColor: Colors.white10, - tabs: [ - Tab(text: controller.myTeam.toUpperCase(), height: 40 * sf), - Tab(text: controller.opponentTeam.toUpperCase(), height: 40 * sf), - ], - ), - - Expanded( - child: Container( - width: double.infinity, - color: Colors.black12, - child: ValueListenableBuilder( - valueListenable: controller.durationNotifier, - builder: (context, duration, _) { - return TabBarView( - physics: const NeverScrollableScrollPhysics(), - children: [ - _buildStatsTable(controller.myCourt + controller.myBench, controller, sf), - _buildStatsTable(controller.oppCourt + controller.oppBench, controller, sf), - ], - ); - } - ), - ), - ), - ], - ), - ), - ), - ); - } - ); - } - - Widget _buildStatsTable(List teamPlayers, PlacarController ctrl, double sf) { - return LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - scrollDirection: Axis.vertical, - physics: const BouncingScrollPhysics(), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - physics: const ClampingScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: constraints.maxWidth), - child: DataTable( - headingRowColor: WidgetStateProperty.all(AppTheme.placarListCard), - columnSpacing: 18 * sf, - horizontalMargin: 16 * sf, - headingRowHeight: 45 * sf, - dataRowMinHeight: 40 * sf, - dataRowMaxHeight: 45 * sf, - headingTextStyle: TextStyle(color: Colors.white70, fontWeight: FontWeight.bold, fontSize: 13 * sf), - dataTextStyle: TextStyle(color: Colors.white, fontSize: 13 * sf), - columns: const [ - DataColumn(label: Text('JOGADOR')), - DataColumn(label: Text('MIN')), - DataColumn(label: Text('PTS')), - DataColumn(label: Text('REB')), - DataColumn(label: Text('AST')), - DataColumn(label: Text('STL')), - DataColumn(label: Text('BLK')), - DataColumn(label: Text('TOV')), - DataColumn(label: Text('FLS')), - DataColumn(label: Text('SO')), - DataColumn(label: Text('IL')), - DataColumn(label: Text('LI')), - DataColumn(label: Text('PA')), - DataColumn(label: Text('3S')), - DataColumn(label: Text('DR')), - DataColumn(label: Text('FG')), - ], - rows: teamPlayers.where((id) => !id.startsWith("fake_")).map((id) { - final name = ctrl.playerNames[id] ?? "---"; - final s = ctrl.playerStats[id]!; - - int totalSecs = s['sec'] ?? 0; - int minutes = totalSecs ~/ 60; - int seconds = totalSecs % 60; - String timeStr = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; - - final rebs = s['orb']! + s['drb']!; - final fgText = "${s['fgm']}/${s['fga']}"; - - return DataRow( - cells: [ - DataCell(Text(name, style: const TextStyle(fontWeight: FontWeight.bold))), - DataCell(Text(timeStr, style: const TextStyle(color: Colors.white70))), - DataCell(Text(s['pts'].toString(), style: TextStyle(color: AppTheme.warningAmber, fontWeight: FontWeight.bold, fontSize: 14 * sf))), - DataCell(Text(rebs.toString())), - DataCell(Text(s['ast'].toString())), - DataCell(Text(s['stl'].toString())), - DataCell(Text(s['blk'].toString())), - DataCell(Text(s['tov'].toString(), style: const TextStyle(color: Colors.redAccent))), - DataCell(Text(s['fls'].toString())), - DataCell(Text((s['so'] ?? 0).toString(), style: const TextStyle(color: Colors.greenAccent))), - DataCell(Text((s['il'] ?? 0).toString(), style: const TextStyle(color: Colors.lightBlue))), - DataCell(Text((s['li'] ?? 0).toString(), style: const TextStyle(color: Colors.orangeAccent))), - DataCell(Text((s['pa'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), - DataCell(Text((s['tres_s'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), - DataCell(Text((s['dr'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), - DataCell(Text(fgText, style: const TextStyle(color: Colors.white54))), - ], - ); - }).toList(), - ), - ), - ), - ); - } - ); - } } \ No newline at end of file diff --git a/lib/widgets/placar_widgets.dart b/lib/widgets/placar_widgets.dart index b923da8..f3177c9 100644 --- a/lib/widgets/placar_widgets.dart +++ b/lib/widgets/placar_widgets.dart @@ -5,47 +5,364 @@ import '../classe/theme.dart'; import '../controllers/placar_controller.dart'; import 'package:playmaker/zone_map_dialog.dart'; +// ============================================================================== +// WIDGETS COMPONENTIZADOS E POP-UPS +// ============================================================================== + +class ActionSubtypeDialog extends StatelessWidget { + final String title; + final Map options; + final Function(String) onSelected; + final double sf; + const ActionSubtypeDialog({super.key, required this.title, required this.options, required this.onSelected, required this.sf}); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + width: MediaQuery.of(context).size.width * 0.55, + decoration: BoxDecoration( + color: AppTheme.placarDarkSurface, + borderRadius: BorderRadius.circular(12 * sf), + border: Border.all(color: AppTheme.warningAmber, width: 1.5 * sf), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 12 * sf), + decoration: BoxDecoration( + color: AppTheme.placarListCard, + borderRadius: BorderRadius.vertical(top: Radius.circular(10 * sf)), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Align( + alignment: Alignment.center, + child: Text( + title, + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf), + ), + ), + Align( + alignment: Alignment.centerRight, + child: InkWell( + onTap: () => Navigator.pop(context), + child: Container( + padding: EdgeInsets.all(4 * sf), + decoration: const BoxDecoration(color: Colors.white24, shape: BoxShape.circle), + child: Icon(Icons.close, color: Colors.white, size: 16 * sf), + ), + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 20 * sf, horizontal: 15 * sf), + child: Wrap( + spacing: 12 * sf, + runSpacing: 15 * sf, + alignment: WrapAlignment.center, + children: options.entries.map((e) => SizedBox( + width: 110 * sf, + height: 60 * sf, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.placarTimerBg, + foregroundColor: Colors.white, + padding: EdgeInsets.all(6 * sf), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8 * sf), + side: BorderSide(color: Colors.white12, width: 1 * sf), + ), + ), + onPressed: () => onSelected(e.key), // Retorna a chave correta (ex: "tov_3s") + child: Text( + e.value, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12 * sf), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + )).toList(), + ), + ), + ], + ), + ), + ); + } +} + +void showFoulVictimDialog(BuildContext context, PlacarController controller, bool isCommitterOpponent, String committerId, String foulType, double sf) { + final victimCourt = isCommitterOpponent ? controller.myCourt : controller.oppCourt; + final prefixCommitter = isCommitterOpponent ? "player_opp_" : "player_my_"; + final prefixVictim = isCommitterOpponent ? "player_my_" : "player_opp_"; + final victimsColor = isCommitterOpponent ? AppTheme.myTeamBlue : AppTheme.oppTeamRed; + final possibleVictims = victimCourt.where((id) => !id.startsWith("fake_")).toList(); + + // FunΓ§Γ£o interna para verificar se o jogador tem de sair + void checkFouledOut() { + final fouls = controller.playerStats[committerId]?["fls"] ?? 0; + final isCourt = isCommitterOpponent ? controller.oppCourt.contains(committerId) : controller.myCourt.contains(committerId); + + if (fouls >= 5 && isCourt) { + Future.delayed(const Duration(milliseconds: 300), () { + if (!context.mounted) return; + showDialog( + context: context, + barrierDismissible: false, // Obriga a fazer a substituiΓ§Γ£o + builder: (ctx) => SubstitutionDialog( + controller: controller, + isOpponent: isCommitterOpponent, + sf: sf, + forcedStarterId: committerId, // Passamos o jogador que foi expulso + ), + ); + }); + } + } + + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + width: MediaQuery.of(context).size.width * 0.60, + decoration: BoxDecoration( + color: AppTheme.placarDarkSurface, + borderRadius: BorderRadius.circular(12 * sf), + border: Border.all(color: AppTheme.warningAmber, width: 1.5 * sf), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 12 * sf), + decoration: BoxDecoration( + color: AppTheme.placarListCard, + borderRadius: BorderRadius.vertical(top: Radius.circular(10 * sf)), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Align( + alignment: Alignment.center, + child: Text( + "Quem sofreu a falta?", + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf), + ), + ), + Align( + alignment: Alignment.centerRight, + child: InkWell( + onTap: () => Navigator.pop(ctx), + child: Container( + padding: EdgeInsets.all(4 * sf), + decoration: const BoxDecoration(color: Colors.white24, shape: BoxShape.circle), + child: Icon(Icons.close, color: Colors.white, size: 16 * sf), + ), + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 20 * sf, horizontal: 10 * sf), + child: Column( + children: [ + Wrap( + spacing: 10 * sf, + runSpacing: 10 * sf, + alignment: WrapAlignment.center, + children: possibleVictims.map((id) { + final name = controller.playerNames[id] ?? "Desconhecido"; + final number = controller.playerNumbers[id] ?? "0"; + + return InkWell( + onTap: () { + Navigator.pop(ctx); + controller.registerFoul("$prefixCommitter$committerId", foulType, "$prefixVictim$id"); + checkFouledOut(); // Verifica 5 faltas! + }, + child: Container( + width: 80 * sf, + padding: EdgeInsets.all(6 * sf), + decoration: BoxDecoration( + color: victimsColor.withOpacity(0.2), + border: Border.all(color: victimsColor, width: 1.5 * sf), + borderRadius: BorderRadius.circular(12 * sf), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + backgroundColor: victimsColor, + radius: 16 * sf, + child: Text(number, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * sf)), + ), + SizedBox(height: 6 * sf), + Text(name, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 10 * sf), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis), + ], + ), + ), + ); + }).toList(), + ), + SizedBox(height: 15 * sf), + const Divider(color: Colors.white24), + SizedBox(height: 8 * sf), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.placarTimerBg, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 10 * sf), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8 * sf), + side: BorderSide(color: Colors.white12, width: 1 * sf), + ), + ), + icon: Icon(Icons.group, color: Colors.white, size: 16 * sf), + label: Text("Equipa / Sem VΓ­tima EspecΓ­fica", style: TextStyle(fontSize: 12 * sf)), + onPressed: () { + Navigator.pop(ctx); + controller.registerFoul("$prefixCommitter$committerId", foulType, ""); + checkFouledOut(); // Verifica 5 faltas! + }, + ) + ], + ), + ), + ], + ), + ), + ), + ); +} + void showAssistDialog(BuildContext context, PlacarController controller, bool isOpponent, String scorerId, double sf) { final teamCourt = isOpponent ? controller.oppCourt : controller.myCourt; final prefix = isOpponent ? "player_opp_" : "player_my_"; + final teamColor = isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue; final possibleAssistants = teamCourt.where((id) => id != scorerId && !id.startsWith("fake_")).toList(); - if (possibleAssistants.isEmpty) return; showDialog( context: context, barrierDismissible: false, - builder: (ctx) => AlertDialog( - backgroundColor: Theme.of(context).colorScheme.surface, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)), - title: Text("Houve AssistΓͺncia?", style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold, fontSize: 18 * sf)), - content: SingleChildScrollView( + builder: (ctx) => Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + width: MediaQuery.of(context).size.width * 0.55, + decoration: BoxDecoration( + color: AppTheme.placarDarkSurface, + borderRadius: BorderRadius.circular(12 * sf), + border: Border.all(color: AppTheme.warningAmber, width: 1.5 * sf), + ), child: Column( mainAxisSize: MainAxisSize.min, children: [ - ...possibleAssistants.map((id) { - final name = controller.playerNames[id] ?? "Desconhecido"; - final number = controller.playerNumbers[id] ?? "0"; - return ListTile( - leading: CircleAvatar( - backgroundColor: isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue, - child: Text(number, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), - ), - title: Text(name, style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)), - onTap: () { - Navigator.pop(ctx); - controller.commitStat("add_ast", "$prefix$id"); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('AssistΓͺncia: $name'), duration: const Duration(seconds: 1), backgroundColor: AppTheme.successGreen)); - }, - ); - }), - Divider(color: Colors.grey.withOpacity(0.3)), - ListTile( - leading: const CircleAvatar(backgroundColor: Colors.grey, child: Icon(Icons.person_off, color: Colors.white)), - title: Text("Isolado (Sem assistΓͺncia)", style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontStyle: FontStyle.italic)), - onTap: () => Navigator.pop(ctx), - ) + Container( + padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 12 * sf), + decoration: BoxDecoration( + color: AppTheme.placarListCard, + borderRadius: BorderRadius.vertical(top: Radius.circular(10 * sf)), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Align( + alignment: Alignment.center, + child: Text( + "Houve AssistΓͺncia?", + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf), + ), + ), + Align( + alignment: Alignment.centerRight, + child: InkWell( + onTap: () => Navigator.pop(ctx), + child: Container( + padding: EdgeInsets.all(4 * sf), + decoration: const BoxDecoration(color: Colors.white24, shape: BoxShape.circle), + child: Icon(Icons.close, color: Colors.white, size: 16 * sf), + ), + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 20 * sf, horizontal: 10 * sf), + child: Column( + children: [ + Wrap( + spacing: 10 * sf, + runSpacing: 10 * sf, + alignment: WrapAlignment.center, + children: possibleAssistants.map((id) { + final name = controller.playerNames[id] ?? "Desconhecido"; + final number = controller.playerNumbers[id] ?? "0"; + + return InkWell( + onTap: () { + Navigator.pop(ctx); + controller.commitStat("add_ast", "$prefix$id"); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('AssistΓͺncia: $name'), duration: const Duration(seconds: 1), backgroundColor: AppTheme.successGreen)); + }, + child: Container( + width: 75 * sf, + padding: EdgeInsets.all(6 * sf), + decoration: BoxDecoration( + color: teamColor.withOpacity(0.2), + border: Border.all(color: teamColor, width: 1.5 * sf), + borderRadius: BorderRadius.circular(12 * sf), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + backgroundColor: teamColor, + radius: 16 * sf, + child: Text(number, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * sf)), + ), + SizedBox(height: 6 * sf), + Text(name, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 10 * sf), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis), + ], + ), + ), + ); + }).toList(), + ), + SizedBox(height: 15 * sf), + const Divider(color: Colors.white24), + SizedBox(height: 8 * sf), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.placarTimerBg, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 10 * sf), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8 * sf), + side: BorderSide(color: Colors.white12, width: 1 * sf), + ), + ), + icon: Icon(Icons.person_off, color: Colors.white, size: 16 * sf), + label: Text("Isolado (Sem assistΓͺncia)", style: TextStyle(fontSize: 12 * sf)), + onPressed: () => Navigator.pop(ctx), + ) + ], + ), + ), ], ), ), @@ -58,208 +375,184 @@ class TopScoreboard extends StatelessWidget { final double sf; const TopScoreboard({super.key, required this.controller, required this.sf}); - + @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.symmetric(vertical: 6 * sf, horizontal: 20 * sf), decoration: BoxDecoration( - color: AppTheme.placarDarkSurface, - borderRadius: BorderRadius.only(bottomLeft: Radius.circular(22 * sf), bottomRight: Radius.circular(22 * sf)), - border: Border.all(color: Colors.white, width: 2.0 * sf), + color: AppTheme.placarDarkSurface, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(22 * sf), + bottomRight: Radius.circular(22 * sf)), + border: Border.all(color: Colors.white, width: 2.0 * sf), ), child: Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.min, children: [ - _buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, AppTheme.myTeamBlue, false, sf), + _buildTeamSection( + controller.myTeam, + controller.myScore, + controller.myFouls, + controller.myTimeoutsUsed, + AppTheme.myTeamBlue, + false, + sf), SizedBox(width: 20 * sf), Column( mainAxisSize: MainAxisSize.min, children: [ Container( - padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 4 * sf), - decoration: BoxDecoration(color: AppTheme.placarTimerBg, borderRadius: BorderRadius.circular(9 * sf)), + padding: EdgeInsets.symmetric( + horizontal: 14 * sf, vertical: 4 * sf), + decoration: BoxDecoration( + color: AppTheme.placarTimerBg, + borderRadius: BorderRadius.circular(9 * sf)), child: ValueListenableBuilder( valueListenable: controller.durationNotifier, builder: (context, duration, child) { - String formatTime = "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}"; - return Text(formatTime, style: TextStyle(color: Colors.white, fontSize: 24 * sf, fontWeight: FontWeight.w900, fontFamily: 'monospace', letterSpacing: 1.5 * sf)); - } + String formatTime = + "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}"; + return Text(formatTime, + style: TextStyle( + color: Colors.white, + fontSize: 24 * sf, + fontWeight: FontWeight.w900, + fontFamily: 'monospace', + letterSpacing: 1.5 * sf)); + }, ), ), SizedBox(height: 4 * sf), - Text("PERÍODO ${controller.currentQuarter}", style: TextStyle(color: AppTheme.warningAmber, fontSize: 12 * sf, fontWeight: FontWeight.w900)), + Text("PERÍODO ${controller.currentQuarter}", + style: TextStyle( + color: AppTheme.warningAmber, + fontSize: 12 * sf, + fontWeight: FontWeight.w900)), ], ), SizedBox(width: 20 * sf), - _buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, AppTheme.oppTeamRed, true, sf), + _buildTeamSection( + controller.opponentTeam, + controller.opponentScore, + controller.opponentFouls, + controller.opponentTimeoutsUsed, + AppTheme.oppTeamRed, + true, + sf), ], ), ); } - Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp, double sf) { + Widget _buildTeamSection(String name, int score, int fouls, int timeouts, + Color color, bool isOpp, double sf) { int displayFouls = fouls > 5 ? 5 : fouls; final timeoutIndicators = Row( mainAxisSize: MainAxisSize.min, - children: List.generate(3, (index) => Container( - margin: EdgeInsets.symmetric(horizontal: 2.5 * sf), width: 10 * sf, height: 10 * sf, - decoration: BoxDecoration(shape: BoxShape.circle, color: index < timeouts ? AppTheme.warningAmber : Colors.grey.shade600, border: Border.all(color: Colors.white54, width: 1.0 * sf)), - )), + children: List.generate( + 3, + (index) => Container( + margin: EdgeInsets.symmetric(horizontal: 2.5 * sf), + width: 10 * sf, + height: 10 * sf, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: index < timeouts + ? AppTheme.warningAmber + : Colors.grey.shade600, + border: + Border.all(color: Colors.white54, width: 1.0 * sf)), + )), ); List content = [ - Column(children: [_scoreBox(score, color, sf), SizedBox(height: 5 * sf), timeoutIndicators]), + Column(children: [ + _scoreBox(score, color, sf), + SizedBox(height: 5 * sf), + timeoutIndicators + ]), SizedBox(width: 12 * sf), Column( - crossAxisAlignment: isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end, + crossAxisAlignment: + isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end, children: [ - Text(name.toUpperCase(), style: TextStyle(color: Colors.white, fontSize: 16 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.0 * sf)), + Text(name.toUpperCase(), + style: TextStyle( + color: Colors.white, + fontSize: 16 * sf, + fontWeight: FontWeight.w900, + letterSpacing: 1.0 * sf)), SizedBox(height: 3 * sf), - Text("FALTAS: $displayFouls", style: TextStyle(color: displayFouls >= 5 ? AppTheme.actionMiss : AppTheme.warningAmber, fontSize: 11 * sf, fontWeight: FontWeight.bold)), + Text("FALTAS: $displayFouls", + style: TextStyle( + color: displayFouls >= 5 + ? AppTheme.actionMiss + : AppTheme.warningAmber, + fontSize: 11 * sf, + fontWeight: FontWeight.bold)), ], ) ]; - return Row(crossAxisAlignment: CrossAxisAlignment.center, children: isOpp ? content : content.reversed.toList()); + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: isOpp ? content : content.reversed.toList()); } Widget _scoreBox(int score, Color color, double sf) => Container( - width: 45 * sf, height: 35 * sf, alignment: Alignment.center, - decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(6 * sf)), - child: Text(score.toString(), style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.w900)), - ); + width: 45 * sf, + height: 35 * sf, + alignment: Alignment.center, + decoration: BoxDecoration( + color: color, borderRadius: BorderRadius.circular(6 * sf)), + child: Text(score.toString(), + style: TextStyle( + color: Colors.white, + fontSize: 20 * sf, + fontWeight: FontWeight.w900)), + ); } -class BenchPopup extends StatelessWidget { - final PlacarController controller; - final bool isOpponent; - final double sf; - - const BenchPopup({super.key, required this.controller, required this.isOpponent, required this.sf}); - - @override - Widget build(BuildContext context) { - final bench = isOpponent ? controller.oppBench : controller.myBench; - final teamColor = isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue; - final prefix = isOpponent ? "bench_opp_" : "bench_my_"; - final teamName = isOpponent ? controller.opponentTeam : controller.myTeam; - - return Container( - width: 280 * sf, - padding: EdgeInsets.all(12 * sf), - decoration: BoxDecoration( - color: AppTheme.placarDarkSurface.withOpacity(0.95), - borderRadius: BorderRadius.circular(16 * sf), - border: Border.all(color: teamColor, width: 2 * sf), - boxShadow: [BoxShadow(color: Colors.black54, blurRadius: 10 * sf, spreadRadius: 2 * sf)], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("SUPLENTES: ${teamName.toUpperCase()}", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12 * sf)), - InkWell( - onTap: () { - if (isOpponent) { controller.showOppBench = false; } - else { controller.showMyBench = false; } - controller.notifyListeners(); - }, - child: Icon(Icons.close, color: Colors.white70, size: 20 * sf), - ) - ], - ), - Divider(color: Colors.white24, height: 16 * sf), - - Wrap( - spacing: 12 * sf, - runSpacing: 12 * sf, - alignment: WrapAlignment.center, - children: bench.map((playerId) { - final playerName = controller.playerNames[playerId] ?? "Erro"; - final num = controller.playerNumbers[playerId] ?? "0"; - final int fouls = controller.playerStats[playerId]?["fls"] ?? 0; - final bool isFouledOut = fouls >= 5; - - String shortName = playerName.length > 8 ? "${playerName.substring(0, 7)}." : playerName; - - Widget avatarUI = Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircleAvatar( - radius: 20 * sf, - backgroundColor: isFouledOut ? Colors.grey.shade800 : teamColor, - child: Text(num, style: TextStyle(color: isFouledOut ? Colors.red.shade300 : Colors.white, fontSize: 16 * sf, fontWeight: FontWeight.bold, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), - ), - SizedBox(height: 4 * sf), - Text(shortName, style: TextStyle(color: Colors.white, fontSize: 10 * sf, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis), - ], - ); - - if (isFouledOut) { - return GestureDetector(onTap: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('πŸ›‘ $playerName expulso!'), backgroundColor: AppTheme.actionMiss)), child: avatarUI); - } - - return Draggable( - data: "$prefix$playerId", - feedback: Material(color: Colors.transparent, child: CircleAvatar(radius: 26 * sf, backgroundColor: teamColor, child: Text(num, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18 * sf)))), - childWhenDragging: Opacity(opacity: 0.3, child: avatarUI), - child: avatarUI, - ); - }).toList(), - ), - ], - ), - ); - } -} - -// ============================================================================ -// SHIRT PAINTER β€” camisola desenhada com CustomPainter -// ============================================================================ class ShirtPainter extends CustomPainter { final Color color; final bool isFouledOut; const ShirtPainter({required this.color, this.isFouledOut = false}); - + @override void paint(Canvas canvas, Size size) { final double w = size.width; final double h = size.height; final Color shirtColor = isFouledOut ? Colors.grey.shade700 : color; - - final paint = Paint()..color = shirtColor..style = PaintingStyle.fill; - final strokePaint = Paint() - ..color = Colors.white.withOpacity(0.25) + + final paint = Paint() + ..color = shirtColor + ..style = PaintingStyle.fill; + + final trimPaint = Paint() + ..color = Colors.white ..style = PaintingStyle.stroke - ..strokeWidth = w * 0.04; - + ..strokeWidth = w * 0.04 + ..strokeJoin = StrokeJoin.round; + final path = Path(); - // Mangas - path.moveTo(w * 0.18, h * 0.18); - path.lineTo(w * 0.00, h * 0.42); - path.lineTo(w * 0.20, h * 0.52); - path.lineTo(w * 0.20, h * 0.95); - path.lineTo(w * 0.80, h * 0.95); - path.lineTo(w * 0.80, h * 0.52); - path.lineTo(w * 1.00, h * 0.42); - path.lineTo(w * 0.82, h * 0.18); - // Gola em V - path.quadraticBezierTo(w * 0.68, h * 0.32, w * 0.50, h * 0.30); - path.quadraticBezierTo(w * 0.32, h * 0.32, w * 0.18, h * 0.18); + path.moveTo(w * 0.32, h * 0.10); + path.lineTo(w * 0.18, h * 0.10); + path.quadraticBezierTo(w * 0.28, h * 0.35, w * 0.05, h * 0.55); + path.lineTo(w * 0.15, h * 1.1); + path.lineTo(w * 0.85, h * 1.1); + path.lineTo(w * 0.95, h * 0.55); + path.quadraticBezierTo(w * 0.72, h * 0.35, w * 0.82, h * 0.10); + path.lineTo(w * 0.68, h * 0.10); + path.quadraticBezierTo(w * 0.50, h * 0.45, w * 0.32, h * 0.10); path.close(); - + canvas.drawPath(path, paint); - canvas.drawPath(path, strokePaint); + canvas.drawPath(path, trimPaint); } - + @override bool shouldRepaint(ShirtPainter old) => old.color != color || old.isFouledOut != isFouledOut; } -// ============================================================================ -// CARD DO JOGADOR NO CAMPO -// ============================================================================ class PlayerCourtCard extends StatelessWidget { final PlacarController controller; final String playerId; @@ -267,16 +560,15 @@ class PlayerCourtCard extends StatelessWidget { final double sf; const PlayerCourtCard({super.key, required this.controller, required this.playerId, required this.isOpponent, required this.sf}); - + @override Widget build(BuildContext context) { final teamColor = isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue; - final realName = controller.playerNames[playerId] ?? "Erro"; final stats = controller.playerStats[playerId]!; final number = controller.playerNumbers[playerId]!; final prefix = isOpponent ? "player_opp_" : "player_my_"; - + return Draggable( data: "$prefix$playerId", feedback: Material( @@ -313,6 +605,84 @@ class PlayerCourtCard extends StatelessWidget { ), ); } + // ─── NOVO POP-UP PARA AS FALTAS ─── + else if (action == "add_foul") { + showDialog( + context: context, + builder: (ctx) => ActionSubtypeDialog( + title: "TIPO DE FALTA", + options: const { + "Defensiva": "Falta Defensiva", + "Ofensiva": "Falta Ofensiva", + "TΓ©cnica": "Falta TΓ©cnica", + "Antidesportiva": "Antidesportiva", + "Desqualificante": "Desqualificante", + }, + sf: sf, + onSelected: (foulType) { + Navigator.pop(ctx); + // Depois de escolher o tipo de falta, abre o pop-up a perguntar quem sofreu + showFoulVictimDialog(context, controller, isOpponent, playerId, foulType, sf); + }, + ), + ); + } + // ─── POP-UPS PARA TOV, STL E BLK ─── + else if (action == "add_tov") { + showDialog( + context: context, + builder: (ctx) => ActionSubtypeDialog( + title: "TIPO DE TURNOVER", + options: const { + "tov_3s": "3 Segundos", + "tov_clock": "RelΓ³gio LanΓ§.", + "tov_travel": "Passos", + "tov_double": "Dribles Duplos", + "tov_badpass": "Passe Ruim", + }, + sf: sf, + onSelected: (subAction) { + Navigator.pop(ctx); + controller.handleActionDrag(context, subAction, "$prefix$playerId"); + }, + ), + ); + } + else if (action == "add_stl") { + showDialog( + context: context, + builder: (ctx) => ActionSubtypeDialog( + title: "TIPO DE ROUBO", + options: const { + "stl_steal": "Roubo de Bola", + "stl_intercept": "InterceΓ§Γ£o LanΓ§.", + }, + sf: sf, + onSelected: (subAction) { + Navigator.pop(ctx); + controller.handleActionDrag(context, subAction, "$prefix$playerId"); + }, + ), + ); + } + else if (action == "add_blk") { + showDialog( + context: context, + builder: (ctx) => ActionSubtypeDialog( + title: "DESARME", + options: const { + "blk_made": "Fez o Desarme", + "blk_suffered": "Sofreu Desarme", + }, + sf: sf, + onSelected: (subAction) { + Navigator.pop(ctx); + controller.handleActionDrag(context, subAction, "$prefix$playerId"); + }, + ), + ); + } + // ─── FIM DOS POP-UPS ESPECIAIS ─── else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) { controller.handleActionDrag(context, action, "$prefix$playerId"); } @@ -329,22 +699,20 @@ class PlayerCourtCard extends StatelessWidget { ), ); } - -Widget _playerCardUI(String number, String displayNameStr, Map stats, Color teamColor, bool isSubbing, bool isActionHover, double sf) { + + Widget _playerCardUI(String number, String displayNameStr, Map stats, Color teamColor, bool isSubbing, bool isActionHover, double sf) { bool isFouledOut = stats["fls"]! >= 5; Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white; Color borderColor = isFouledOut ? AppTheme.actionMiss : Colors.transparent; if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = AppTheme.myTeamBlue; } else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = AppTheme.actionPoints; } - + int fgm = stats["fgm"]!; int fga = stats["fga"]!; String fgPercent = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) : "0"; String displayName = displayNameStr.length > 12 ? "${displayNameStr.substring(0, 10)}..." : displayNameStr; - - // Tamanho da camisola ajustado para ficar perfeito no cartΓ£o - final double shirtSize = 42 * sf; - + final double shirtSize = 40 * sf; + return Container( padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 6 * sf), decoration: BoxDecoration( @@ -356,9 +724,8 @@ Widget _playerCardUI(String number, String displayNameStr, Map stat child: IntrinsicHeight( child: Row( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, // Centra verticalmente a camisola com o texto + crossAxisAlignment: CrossAxisAlignment.center, children: [ - // ── APENAS A CAMISOLA (Sem quadrado de fundo) ── SizedBox( width: shirtSize, height: shirtSize, @@ -388,9 +755,7 @@ Widget _playerCardUI(String number, String displayNameStr, Map stat ], ), ), - SizedBox(width: 8 * sf), // EspaΓ§o entre a camisola e as estatΓ­sticas - - // ── EstatΓ­sticas ───────────────────────────────────── + SizedBox(width: 8 * sf), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, @@ -409,24 +774,320 @@ Widget _playerCardUI(String number, String displayNameStr, Map stat } } +class SubstitutionDialog extends StatefulWidget { + final PlacarController controller; + final bool isOpponent; + final double sf; + final String? forcedStarterId; // <--- ADICIONADO PARA EXPULSΓ•ES + + const SubstitutionDialog({ + super.key, + required this.controller, + required this.isOpponent, + required this.sf, + this.forcedStarterId, + }); + + @override + State createState() => _SubstitutionDialogState(); +} + +class _SubstitutionDialogState extends State { + String? _selectedStarterId; + String? _selectedBenchId; + + PlacarController get ctrl => widget.controller; + bool get isOpp => widget.isOpponent; + double get sf => widget.sf; + List get court => isOpp ? ctrl.oppCourt : ctrl.myCourt; + List get bench => isOpp ? ctrl.oppBench : ctrl.myBench; + Color get teamColor => isOpp ? AppTheme.oppTeamRed : AppTheme.myTeamBlue; + String get teamName => isOpp ? ctrl.opponentTeam : ctrl.myTeam; + bool get canConfirm => _selectedStarterId != null && _selectedBenchId != null; + bool get isForced => widget.forcedStarterId != null; // NOVO + + @override + void initState() { + super.initState(); + // Se for obrigado a sair, jΓ‘ aparece selecionado! + if (isForced) { + _selectedStarterId = widget.forcedStarterId; + } + } + + void _confirmSwap() { + if (!canConfirm) return; + final benchPrefix = isOpp ? "bench_opp_" : "bench_my_"; + ctrl.handleSubbing( + context, + "$benchPrefix$_selectedBenchId", + _selectedStarterId!, + isOpp, + ); + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + const activeColor = Color(0xFF22C55E); + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.all(16 * sf), + child: Container( + width: 420 * sf, + decoration: BoxDecoration( + color: const Color(0xFF1A1F2E), + borderRadius: BorderRadius.circular(14 * sf), + border: Border.all(color: isForced ? AppTheme.actionMiss : const Color(0xFF2D3450), width: 2), // Borda vermelha se for expulsΓ£o + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 10 * sf), + decoration: BoxDecoration( + color: isForced ? AppTheme.actionMiss.withOpacity(0.8) : const Color(0xFF1E2540), // Fundo vermelho no tΓ­tulo + borderRadius: BorderRadius.vertical(top: Radius.circular(12 * sf)), + border: const Border(bottom: BorderSide(color: Color(0xFF2D3450))), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isForced ? "SUBSTITUIÇÃO OBRIGATΓ“RIA (5 Faltas)" : "SubstituiΓ§Γ£o β€” ${teamName.toUpperCase()}", + style: TextStyle(color: Colors.white, fontSize: 13 * sf, fontWeight: FontWeight.w600), + ), + if (!isForced) // Esconde o "X" de fechar se for forΓ§ado + InkWell( + onTap: () => Navigator.pop(context), + child: Container( + padding: EdgeInsets.all(4 * sf), + decoration: const BoxDecoration(color: Colors.white24, shape: BoxShape.circle), + child: Icon(Icons.close, color: Colors.white, size: 14 * sf), + ), + ), + ], + ), + ), + _sectionLabel("Em Campo"), + _playerGrid( + players: court.where((id) => !id.startsWith("fake_")).toList(), + selected: _selectedStarterId, + isStarter: true, + activeColor: activeColor, + onTap: (id) { + if (isForced) return; // Se for forΓ§ado, nΓ£o deixa clicar/desmarcar o titular + setState(() { + _selectedStarterId = _selectedStarterId == id ? null : id; + }); + }, + ), + Divider(color: Colors.white12, height: 1, indent: 10 * sf, endIndent: 10 * sf), + _sectionLabel("Banco de Suplentes"), + _playerGrid( + players: bench.where((id) => !id.startsWith("fake_")).toList(), + selected: _selectedBenchId, + isStarter: false, + activeColor: activeColor, + onTap: (id) { + final fouls = ctrl.playerStats[id]?["fls"] ?? 0; + if (fouls >= 5) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('πŸ›‘ ${ctrl.playerNames[id]} expulso!'), + backgroundColor: AppTheme.actionMiss)); + return; + } + setState(() { + _selectedBenchId = _selectedBenchId == id ? null : id; + }); + }, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf), + child: Text( + _hintText(), + style: TextStyle(color: isForced ? AppTheme.actionMiss : Colors.white38, fontSize: 11 * sf, fontWeight: isForced ? FontWeight.bold : FontWeight.normal), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(12 * sf, 0, 12 * sf, 12 * sf), + child: Row( + children: [ + if (!isForced) // Esconde o botΓ£o de cancelar + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white12, + foregroundColor: Colors.white70, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8 * sf)), + padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 10 * sf), + elevation: 0, + ), + onPressed: () => Navigator.pop(context), + child: Text("Cancelar", style: TextStyle(fontSize: 13 * sf)), + ), + if (!isForced) SizedBox(width: 8 * sf), + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: canConfirm ? activeColor : Colors.white12, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8 * sf)), + padding: EdgeInsets.symmetric(vertical: 10 * sf), + elevation: 0, + ), + onPressed: canConfirm ? _confirmSwap : null, + child: Text(isForced ? "Substituir Jogador" : "Confirmar Troca", style: TextStyle(fontSize: 13 * sf, fontWeight: FontWeight.w600)), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + String _hintText() { + if (isForced && _selectedBenchId == null) { + return "Um jogador atingiu as 5 faltas. Seleciona um suplente obrigatoriamente."; + } else if (_selectedStarterId == null && _selectedBenchId == null) { + return "Seleciona um titular e um suplente para fazer a troca"; + } else if (_selectedStarterId != null && _selectedBenchId == null) { + return "Agora seleciona o suplente que vai entrar"; + } else if (_selectedStarterId == null && _selectedBenchId != null) { + return "Agora seleciona o titular que vai sair"; + } else { + final s = ctrl.playerNames[_selectedStarterId] ?? ""; + final sNum = ctrl.playerNumbers[_selectedStarterId] ?? ""; + final b = ctrl.playerNames[_selectedBenchId] ?? ""; + final bNum = ctrl.playerNumbers[_selectedBenchId] ?? ""; + return "#$sNum $s ↔ #$bNum $b"; + } + } + + Widget _sectionLabel(String label) => Padding( + padding: EdgeInsets.fromLTRB(12 * sf, 8 * sf, 12 * sf, 4 * sf), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + label.toUpperCase(), + style: TextStyle(color: Colors.white38, fontSize: 10 * sf, letterSpacing: 0.8, fontWeight: FontWeight.w500), + ), + ), + ); + + Widget _playerGrid({ + required List players, + required String? selected, + required bool isStarter, + required Color activeColor, + required void Function(String) onTap, + }) { + return Padding( + padding: EdgeInsets.fromLTRB(10 * sf, 0, 10 * sf, 6 * sf), + child: Wrap( + spacing: 6 * sf, + runSpacing: 6 * sf, + children: players.map((id) { + final isSelected = selected == id; + final name = ctrl.playerNames[id] ?? "?"; + final num = ctrl.playerNumbers[id] ?? "0"; + final fouls = ctrl.playerStats[id]?["fls"] ?? 0; + final isFouledOut = fouls >= 5; + final shortName = name.length > 8 ? "${name.substring(0, 7)}." : name; + final bgColor = isSelected ? const Color(0xFF14331F) : isStarter ? const Color(0xFF1E2540) : const Color(0xFF141824); + final borderColor = isSelected ? activeColor : Colors.transparent; + final shirtColor = isSelected ? const Color(0xFF15803D) : isFouledOut ? Colors.grey.shade700 : teamColor; + + return GestureDetector( + onTap: () => onTap(id), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + width: 60 * sf, + padding: EdgeInsets.symmetric(vertical: 6 * sf, horizontal: 2 * sf), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(10 * sf), + border: Border.all(color: borderColor, width: 2), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 36 * sf, + height: 36 * sf, + child: Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + size: Size(36 * sf, 36 * sf), + painter: ShirtPainter(color: shirtColor, isFouledOut: isFouledOut), + ), + Padding( + padding: EdgeInsets.only(top: 36 * sf * 0.15), + child: Text( + num, + style: TextStyle( + color: Colors.white, + fontSize: 36 * sf * 0.38, + fontWeight: FontWeight.w900, + decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none, + shadows: const [Shadow(color: Colors.black45, blurRadius: 2)], + ), + ), + ), + ], + ), + ), + SizedBox(height: 3 * sf), + Text( + shortName, + style: TextStyle(color: Colors.white70, fontSize: 9 * sf, fontWeight: FontWeight.w500), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + if (isFouledOut) + Container( + margin: EdgeInsets.only(top: 2 * sf), + padding: EdgeInsets.symmetric(horizontal: 4 * sf, vertical: 1 * sf), + decoration: BoxDecoration( + color: Colors.red.shade900.withOpacity(0.4), + border: Border.all(color: Colors.red.shade400), + borderRadius: BorderRadius.circular(4 * sf), + ), + child: Text("5 Fls", style: TextStyle(color: Colors.red.shade300, fontSize: 8 * sf)), + ), + ], + ), + ), + ); + }).toList(), + ), + ); + } +} class HeatmapDialog extends StatefulWidget { final List shots; final String myTeamName; final String oppTeamName; - final List myPlayers; - final List oppPlayers; + final List myPlayersIds; + final List oppPlayersIds; final Map> playerStats; - + final Map playerNames; + const HeatmapDialog({ - super.key, + super.key, required this.shots, required this.myTeamName, required this.oppTeamName, - required this.myPlayers, - required this.oppPlayers, + required this.myPlayersIds, + required this.oppPlayersIds, required this.playerStats, + required this.playerNames, }); - + @override State createState() => _HeatmapDialogState(); } @@ -434,17 +1095,16 @@ class HeatmapDialog extends StatefulWidget { class _HeatmapDialogState extends State { bool _isMapVisible = false; String _selectedTeam = ''; - String _selectedPlayer = ''; - + String _selectedPlayerId = ''; + @override Widget build(BuildContext context) { - final Color headerColor = const Color(0xFFE88F15); - final Color yellowBackground = const Color(0xFFDFAB00); - + const Color headerColor = Color(0xFFE88F15); + const Color yellowBackground = Color(0xFFDFAB00); final double screenHeight = MediaQuery.of(context).size.height; - final double dialogHeight = screenHeight * 0.95; - final double dialogWidth = dialogHeight * 1.0; - + final double dialogHeight = screenHeight * 0.95; + final double dialogWidth = dialogHeight * 1.0; + return Dialog( backgroundColor: yellowBackground, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), @@ -457,7 +1117,7 @@ class _HeatmapDialogState extends State { ), ); } - + Widget _buildSelectionScreen(Color headerColor) { return Column( children: [ @@ -468,10 +1128,7 @@ class _HeatmapDialogState extends State { child: Stack( alignment: Alignment.center, children: [ - const Text( - "ESCOLHE A EQUIPA OU UM JOGADOR", - style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold), - ), + const Text("ESCOLHE A EQUIPA OU UM JOGADOR", style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold)), Positioned( right: 8, child: InkWell( @@ -486,27 +1143,14 @@ class _HeatmapDialogState extends State { ], ), ), - Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ - Expanded( - child: _buildTeamColumn( - teamName: widget.myTeamName, - players: widget.myPlayers, - teamColor: AppTheme.myTeamBlue, - ), - ), + Expanded(child: _buildTeamColumn(teamName: widget.myTeamName, playerIds: widget.myPlayersIds, teamColor: AppTheme.myTeamBlue)), const SizedBox(width: 8), - Expanded( - child: _buildTeamColumn( - teamName: widget.oppTeamName, - players: widget.oppPlayers, - teamColor: AppTheme.oppTeamRed, - ), - ), + Expanded(child: _buildTeamColumn(teamName: widget.oppTeamName, playerIds: widget.oppPlayersIds, teamColor: AppTheme.oppTeamRed)), ], ), ), @@ -514,21 +1158,17 @@ class _HeatmapDialogState extends State { ], ); } - - Widget _buildTeamColumn({required String teamName, required List players, required Color teamColor}) { - List realPlayers = players.where((p) => !p.startsWith("Sem ")).toList(); - + + Widget _buildTeamColumn({required String teamName, required List playerIds, required Color teamColor}) { + final realPlayerIds = playerIds.where((id) => !id.startsWith("fake_")).toList(); return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), child: Column( children: [ InkWell( onTap: () => setState(() { _selectedTeam = teamName; - _selectedPlayer = 'Todos'; + _selectedPlayerId = 'Todos'; _isMapVisible = true; }), child: Container( @@ -553,22 +1193,22 @@ class _HeatmapDialogState extends State { ), Expanded( child: ListView.separated( - itemCount: realPlayers.length, + itemCount: realPlayerIds.length, separatorBuilder: (context, index) => const Divider(height: 1, color: Colors.black12), itemBuilder: (context, index) { - String p = realPlayers[index]; - int pts = widget.playerStats[p]?['pts'] ?? 0; - + final pId = realPlayerIds[index]; + final pName = widget.playerNames[pId] ?? 'Desconhecido'; + final pts = widget.playerStats[pId]?['pts'] ?? 0; return ListTile( dense: true, visualDensity: VisualDensity.compact, leading: Icon(Icons.person, color: teamColor), - title: Text(p, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)), + title: Text(pName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)), trailing: Text("$pts Pts", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: teamColor)), onTap: () => setState(() { _selectedTeam = teamName; - _selectedPlayer = p; - _isMapVisible = true; + _selectedPlayerId = pId; + _isMapVisible = true; }), ); }, @@ -578,19 +1218,19 @@ class _HeatmapDialogState extends State { ), ); } - + Widget _buildMapScreen(Color headerColor) { - List filteredShots = widget.shots.where((s) { - if (_selectedPlayer != 'Todos') return s.playerName == _selectedPlayer; - if (_selectedTeam == widget.myTeamName) return widget.myPlayers.contains(s.playerName); - if (_selectedTeam == widget.oppTeamName) return widget.oppPlayers.contains(s.playerName); + final filteredShots = widget.shots.where((s) { + if (_selectedPlayerId != 'Todos') return s.playerId == _selectedPlayerId; + if (_selectedTeam == widget.myTeamName) return widget.myPlayersIds.contains(s.playerId); + if (_selectedTeam == widget.oppTeamName) return widget.oppPlayersIds.contains(s.playerId); return true; }).toList(); - - String titleText = _selectedPlayer == 'Todos' - ? "MAPA GERAL: ${_selectedTeam.toUpperCase()}" - : "MAPA: ${_selectedPlayer.toUpperCase()}"; - + + final titleText = _selectedPlayerId == 'Todos' + ? "MAPA GERAL: ${_selectedTeam.toUpperCase()}" + : "MAPA: ${widget.playerNames[_selectedPlayerId]?.toUpperCase() ?? ''}"; + return Column( children: [ Container( @@ -603,60 +1243,53 @@ class _HeatmapDialogState extends State { Positioned( left: 8, child: InkWell( - onTap: () => setState(() => _isMapVisible = false), + onTap: () => setState(() => _isMapVisible = false), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), - child: Row( - children: [ - Icon(Icons.arrow_back, color: headerColor, size: 14), - const SizedBox(width: 4), - Text("VOLTAR", style: TextStyle(color: headerColor, fontWeight: FontWeight.bold, fontSize: 12)), - ], - ), + child: Row(children: [ + Icon(Icons.arrow_back, color: headerColor, size: 14), + const SizedBox(width: 4), + Text("VOLTAR", style: TextStyle(color: headerColor, fontWeight: FontWeight.bold, fontSize: 12)), + ]), ), ), ), - Text( - titleText, - style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold), - ), + Text(titleText, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold)), Positioned( right: 8, child: InkWell( - onTap: () => Navigator.pop(context), + onTap: () => Navigator.pop(context), child: Container( padding: const EdgeInsets.all(4), decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), child: Icon(Icons.close, color: headerColor, size: 16), ), ), - ) + ), ], ), ), Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - return Stack( - children: [ - CustomPaint( - size: Size(constraints.maxWidth, constraints.maxHeight), - painter: HeatmapCourtPainter(), - ), - ...filteredShots.map((shot) => Positioned( - left: (shot.relativeX * constraints.maxWidth) - 8, - top: (shot.relativeY * constraints.maxHeight) - 8, - child: CircleAvatar( - radius: 8, - backgroundColor: shot.isMake ? AppTheme.successGreen : AppTheme.actionMiss, - child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white) - ), - )), - ], - ); - }, - ), + child: LayoutBuilder(builder: (context, constraints) { + return Stack( + children: [ + CustomPaint( + size: Size(constraints.maxWidth, constraints.maxHeight), + painter: HeatmapCourtPainter(), + ), + ...filteredShots.map((shot) => Positioned( + left: (shot.relativeX * constraints.maxWidth) - 8, + top: (shot.relativeY * constraints.maxHeight) - 8, + child: CircleAvatar( + radius: 8, + backgroundColor: shot.isMake ? AppTheme.successGreen : AppTheme.actionMiss, + child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white), + ), + )), + ], + ); + }), ), ], ); @@ -669,30 +1302,28 @@ class HeatmapCourtPainter extends CustomPainter { final double w = size.width; final double h = size.height; final double basketX = w / 2; - final Paint whiteStroke = Paint()..color = Colors.white..style = PaintingStyle.stroke..strokeWidth = 2.0; final Paint blackStroke = Paint()..color = Colors.black87..style = PaintingStyle.stroke..strokeWidth = 2.0; - final double margin = w * 0.10; final double length = h * 0.35; final double larguraDoArco = (w / 2) - margin; final double alturaDoArco = larguraDoArco * 0.30; final double totalArcoHeight = alturaDoArco * 4; - + canvas.drawLine(Offset(margin, 0), Offset(margin, length), whiteStroke); canvas.drawLine(Offset(w - margin, 0), Offset(w - margin, length), whiteStroke); canvas.drawLine(Offset(0, length), Offset(margin, length), whiteStroke); canvas.drawLine(Offset(w - margin, length), Offset(w, length), whiteStroke); canvas.drawArc(Rect.fromCenter(center: Offset(basketX, length), width: larguraDoArco * 2, height: totalArcoHeight), 0, math.pi, false, whiteStroke); - + double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75)); double sYL = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.75)); double sXR = basketX + (larguraDoArco * math.cos(math.pi * 0.25)); double sYR = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.25)); - + canvas.drawLine(Offset(sXL, sYL), Offset(0, h * 0.85), whiteStroke); canvas.drawLine(Offset(sXR, sYR), Offset(w, h * 0.85), whiteStroke); - + final double pW = w * 0.28; final double pH = h * 0.38; canvas.drawRect(Rect.fromLTWH(basketX - pW / 2, 0, pW, pH), blackStroke); @@ -702,26 +1333,22 @@ class HeatmapCourtPainter extends CustomPainter { for (int i = 0; i < 10; i++) { canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), math.pi + (i * 2 * (math.pi / 20)), math.pi / 20, false, blackStroke); } - - canvas.drawLine(Offset(basketX - pW / 2, pH), Offset(sXL, sYL), blackStroke); - canvas.drawLine(Offset(basketX + pW / 2, pH), Offset(sXR, sYR), blackStroke); - + + canvas.drawLine(Offset(basketX - pW / 2, pH), Offset(sXL, sYL), blackStroke); + canvas.drawLine(Offset(basketX + pW / 2, pH), Offset(sXR, sYR), blackStroke); canvas.drawArc(Rect.fromCircle(center: Offset(basketX, h), radius: w * 0.12), math.pi, math.pi, false, blackStroke); canvas.drawCircle(Offset(basketX, h * 0.12), w * 0.02, blackStroke); canvas.drawLine(Offset(basketX - w * 0.08, h * 0.12 - 5), Offset(basketX + w * 0.08, h * 0.12 - 5), blackStroke); } - + @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } -// ============================================================================ -// 5. CAIXA DE HISTΓ“RICO (PLAY-BY-PLAY) -// ============================================================================ class PlayByPlayDialog extends StatelessWidget { final PlacarController controller; const PlayByPlayDialog({super.key, required this.controller}); - + @override Widget build(BuildContext context) { return Dialog( @@ -742,21 +1369,16 @@ class PlayByPlayDialog extends StatelessWidget { ), const Divider(color: Colors.white24), Expanded( - child: controller.playByPlay.isEmpty - ? const Center(child: Text("Ainda nΓ£o hΓ‘ jogadas.", style: TextStyle(color: Colors.white54))) - : ListView.separated( - itemCount: controller.playByPlay.length, - separatorBuilder: (_, __) => const Divider(color: Colors.white10), - itemBuilder: (context, index) { - return Padding( + child: controller.playByPlay.isEmpty + ? const Center(child: Text("Ainda nΓ£o hΓ‘ jogadas.", style: TextStyle(color: Colors.white54))) + : ListView.separated( + itemCount: controller.playByPlay.length, + separatorBuilder: (_, __) => const Divider(color: Colors.white10), + itemBuilder: (context, index) => Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Text( - controller.playByPlay[index], - style: const TextStyle(color: Colors.white, fontSize: 14), - ), - ); - }, - ), + child: Text(controller.playByPlay[index], style: const TextStyle(color: Colors.white, fontSize: 14)), + ), + ), ), ], ), @@ -765,97 +1387,157 @@ class PlayByPlayDialog extends StatelessWidget { } } -// ============================================================================ -// 6. ECRΓƒ DE BOX SCORE (ESTATÍSTICAS GERAIS) -// ============================================================================ class BoxScoreDialog extends StatelessWidget { final PlacarController controller; - const BoxScoreDialog({super.key, required this.controller}); - + final double sf; + + const BoxScoreDialog({super.key, required this.controller, required this.sf}); + @override Widget build(BuildContext context) { - return Dialog( - backgroundColor: AppTheme.placarDarkSurface, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Container( - width: MediaQuery.of(context).size.width * 0.9, - height: MediaQuery.of(context).size.height * 0.9, - padding: const EdgeInsets.all(16), - child: DefaultTabController( - length: 2, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return AnimatedBuilder( + animation: controller, + builder: (context, child) { + return Dialog( + backgroundColor: AppTheme.placarDarkSurface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12 * sf), + side: BorderSide(color: Colors.white24, width: 1 * sf), + ), + insetPadding: EdgeInsets.all(8 * sf), + clipBehavior: Clip.antiAlias, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.98, + height: MediaQuery.of(context).size.height * 0.98, + child: DefaultTabController( + length: 2, + child: Column( children: [ - const Text("BOX SCORE", style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold)), - IconButton(icon: const Icon(Icons.close, color: Colors.white), onPressed: () => Navigator.pop(context)) + Padding( + padding: EdgeInsets.fromLTRB(16 * sf, 16 * sf, 8 * sf, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("BOX SCORE", style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)), + IconButton( + icon: Icon(Icons.close, color: Colors.white, size: 24 * sf), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () => Navigator.pop(context), + ) + ], + ), + ), + SizedBox(height: 8 * sf), + TabBar( + indicatorColor: AppTheme.warningAmber, + labelColor: Colors.white, + unselectedLabelColor: Colors.white54, + labelStyle: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold), + indicatorWeight: 3 * sf, + dividerColor: Colors.white10, + tabs: [ + Tab(text: controller.myTeam.toUpperCase(), height: 40 * sf), + Tab(text: controller.opponentTeam.toUpperCase(), height: 40 * sf), + ], + ), + Expanded( + child: Container( + width: double.infinity, + color: Colors.black12, + child: ValueListenableBuilder( + valueListenable: controller.durationNotifier, + builder: (context, duration, _) { + return TabBarView( + physics: const NeverScrollableScrollPhysics(), + children: [ + _buildStatsTable(controller.myCourt + controller.myBench, controller, sf), + _buildStatsTable(controller.oppCourt + controller.oppBench, controller, sf), + ], + ); + }, + ), + ), + ), ], ), - TabBar( - indicatorColor: AppTheme.warningAmber, - labelColor: Colors.white, - unselectedLabelColor: Colors.white54, - tabs: [ - Tab(text: controller.myTeam.toUpperCase()), - Tab(text: controller.opponentTeam.toUpperCase()), - ], - ), - const SizedBox(height: 10), - Expanded( - child: TabBarView( - children: [ - _buildStatsTable(controller.myCourt + controller.myBench, controller), - _buildStatsTable(controller.oppCourt + controller.oppBench, controller), - ], - ), - ), - ], + ), + ), + ); + }, + ); + } + + Widget _buildStatsTable(List teamPlayers, PlacarController ctrl, double sf) { + return LayoutBuilder(builder: (context, constraints) { + return SingleChildScrollView( + scrollDirection: Axis.vertical, + physics: const BouncingScrollPhysics(), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const ClampingScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: DataTable( + headingRowColor: WidgetStateProperty.all(AppTheme.placarListCard), + columnSpacing: 18 * sf, + horizontalMargin: 16 * sf, + headingRowHeight: 45 * sf, + dataRowMinHeight: 40 * sf, + dataRowMaxHeight: 45 * sf, + headingTextStyle: TextStyle(color: Colors.white70, fontWeight: FontWeight.bold, fontSize: 13 * sf), + dataTextStyle: TextStyle(color: Colors.white, fontSize: 13 * sf), + columns: const [ + DataColumn(label: Text('JOGADOR')), + DataColumn(label: Text('MIN')), + DataColumn(label: Text('PTS')), + DataColumn(label: Text('REB')), + DataColumn(label: Text('AST')), + DataColumn(label: Text('STL')), + DataColumn(label: Text('BLK')), + DataColumn(label: Text('TOV')), + DataColumn(label: Text('FLS')), + DataColumn(label: Text('SO')), + DataColumn(label: Text('IL')), + DataColumn(label: Text('LI')), + DataColumn(label: Text('PA')), + DataColumn(label: Text('3S')), + DataColumn(label: Text('DR')), + DataColumn(label: Text('FG')), + ], + rows: teamPlayers.where((id) => !id.startsWith("fake_")).map((id) { + final name = ctrl.playerNames[id] ?? "---"; + final s = ctrl.playerStats[id]!; + final totalSecs = s['sec'] ?? 0; + final minutes = totalSecs ~/ 60; + final seconds = totalSecs % 60; + final timeStr = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + final rebs = s['orb']! + s['drb']!; + final fgText = "${s['fgm']}/${s['fga']}"; + + return DataRow(cells: [ + DataCell(Text(name, style: const TextStyle(fontWeight: FontWeight.bold))), + DataCell(Text(timeStr, style: const TextStyle(color: Colors.white70))), + DataCell(Text(s['pts'].toString(), style: TextStyle(color: AppTheme.warningAmber, fontWeight: FontWeight.bold, fontSize: 14 * sf))), + DataCell(Text(rebs.toString())), + DataCell(Text(s['ast'].toString())), + DataCell(Text(s['stl'].toString())), + DataCell(Text(s['blk'].toString())), + DataCell(Text(s['tov'].toString(), style: const TextStyle(color: Colors.redAccent))), + DataCell(Text(s['fls'].toString())), + DataCell(Text((s['so'] ?? 0).toString(), style: const TextStyle(color: Colors.greenAccent))), + DataCell(Text((s['il'] ?? 0).toString(), style: const TextStyle(color: Colors.lightBlue))), + DataCell(Text((s['li'] ?? 0).toString(), style: const TextStyle(color: Colors.orangeAccent))), + DataCell(Text((s['pa'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), + DataCell(Text((s['tres_seg'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), // CORRIGIDO PARA MOSTRAR OS 3 SEG NO BOX SCORE + DataCell(Text((s['dr'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), + DataCell(Text(fgText, style: const TextStyle(color: Colors.white54))), + ]); + }).toList(), + ), ), ), - ), - ); - } - - Widget _buildStatsTable(List teamPlayers, PlacarController ctrl) { - return SingleChildScrollView( - scrollDirection: Axis.vertical, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - headingRowColor: WidgetStateProperty.all(Colors.black26), - columns: const [ - DataColumn(label: Text('JOGADOR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), - DataColumn(label: Text('PTS', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), - DataColumn(label: Text('REB', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), - DataColumn(label: Text('AST', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), - DataColumn(label: Text('STL', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), - DataColumn(label: Text('BLK', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), - DataColumn(label: Text('TOV', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), - DataColumn(label: Text('FLS', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), - DataColumn(label: Text('FG', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), - ], - rows: teamPlayers.where((id) => !id.startsWith("fake_")).map((id) { - final name = ctrl.playerNames[id] ?? "---"; - final s = ctrl.playerStats[id]!; - final rebs = s['orb']! + s['drb']!; - final fgText = "${s['fgm']}/${s['fga']}"; - return DataRow( - cells: [ - DataCell(Text(name, style: const TextStyle(color: Colors.white))), - DataCell(Text(s['pts'].toString(), style: const TextStyle(color: AppTheme.warningAmber, fontWeight: FontWeight.bold))), - DataCell(Text(rebs.toString(), style: const TextStyle(color: Colors.white))), - DataCell(Text(s['ast'].toString(), style: const TextStyle(color: Colors.white))), - DataCell(Text(s['stl'].toString(), style: const TextStyle(color: Colors.white))), - DataCell(Text(s['blk'].toString(), style: const TextStyle(color: Colors.white))), - DataCell(Text(s['tov'].toString(), style: const TextStyle(color: Colors.redAccent))), - DataCell(Text(s['fls'].toString(), style: const TextStyle(color: Colors.white))), - DataCell(Text(fgText, style: const TextStyle(color: Colors.white54))), - ], - ); - }).toList(), - ), - ), - ); + ); + }); } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index d5e3b37..0daf6be 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" clock: dependency: transitive description: @@ -468,18 +468,18 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -873,10 +873,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" typed_data: dependency: transitive description: