diff --git a/lib/controllers/game_controller.dart b/lib/controllers/game_controller.dart index 91c68b7..ffa141a 100644 --- a/lib/controllers/game_controller.dart +++ b/lib/controllers/game_controller.dart @@ -5,14 +5,22 @@ class GameController { final _supabase = Supabase.instance.client; // 1. LER JOGOS (Stream em Tempo Real) - Stream> get gamesStream { +Stream> get gamesStream { return _supabase - .from('games') + .from('games') // 1. Fica à escuta da tabela original (Garante o Tempo Real!) .stream(primaryKey: ['id']) - .order('game_date', ascending: false) // Mais recentes primeiro - .map((data) => data.map((json) => Game.fromMap(json)).toList()); + .asyncMap((event) async { + // 2. Sempre que a tabela 'games' mudar (novo jogo, alteração de resultado), + // vamos buscar os dados já misturados com as imagens à nossa View. + final viewData = await _supabase + .from('games_with_logos') + .select() + .order('game_date', ascending: false); + + // 3. Convertemos para a nossa lista de objetos Game + return viewData.map((json) => Game.fromMap(json)).toList(); + }); } - // 2. CRIAR JOGO // Retorna o ID do jogo criado para podermos navegar para o placar Future createGame(String myTeam, String opponent, String season) async { diff --git a/lib/controllers/placar_controller.dart b/lib/controllers/placar_controller.dart new file mode 100644 index 0000000..81c7ac1 --- /dev/null +++ b/lib/controllers/placar_controller.dart @@ -0,0 +1,340 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +class ShotRecord { + final Offset position; + final bool isMake; + ShotRecord(this.position, this.isMake); +} + +class PlacarController { + final String gameId; // O ID real do jogo na base de dados + final String myTeam; + final String opponentTeam; + final VoidCallback onUpdate; + + PlacarController({ + required this.gameId, + required this.myTeam, + required this.opponentTeam, + required this.onUpdate + }); + + bool isLoading = true; + bool isSaving = false; // Para mostrar o ícone de loading a guardar + + int myScore = 0; + int opponentScore = 0; + int myFouls = 0; + int opponentFouls = 0; + int currentQuarter = 1; + int myTimeoutsUsed = 0; + int opponentTimeoutsUsed = 0; + + String? myTeamDbId; // ID da tua equipa na BD + String? oppTeamDbId; // ID da equipa adversária na BD + + List myCourt = []; + List myBench = []; + List oppCourt = []; + List oppBench = []; + + Map playerNumbers = {}; + Map> playerStats = {}; + Map playerDbIds = {}; // NOVO: Mapeia o Nome do jogador -> UUID na base de dados + + bool showMyBench = false; + bool showOppBench = false; + + bool isSelectingShotLocation = false; + String? pendingAction; + String? pendingPlayer; + List matchShots = []; + + Duration duration = const Duration(minutes: 10); + Timer? timer; + bool isRunning = false; + + // --- INICIALIZAÇÃO E BD --- + Future loadPlayers() async { + final supabase = Supabase.instance.client; + try { + // 1. Buscar os IDs das equipas + final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]); + + for (var t in teamsResponse) { + if (t['name'] == myTeam) myTeamDbId = t['id']; + if (t['name'] == opponentTeam) oppTeamDbId = t['id']; + } + + // 2. Buscar os Jogadores + List myPlayers = myTeamDbId != null ? await supabase.from('members').select().eq('team_id', myTeamDbId!).eq('type', 'Jogador') : []; + List oppPlayers = oppTeamDbId != null ? await supabase.from('members').select().eq('team_id', oppTeamDbId!).eq('type', 'Jogador') : []; + + // 3. Registar a tua equipa + for (int i = 0; i < myPlayers.length; i++) { + _registerPlayer( + name: myPlayers[i]['name'].toString(), + number: myPlayers[i]['number']?.toString() ?? "0", + dbId: myPlayers[i]['id'].toString(), // Guarda o UUID real + isMyTeam: true, + isCourt: i < 5 + ); + } + _padTeam(myCourt, myBench, "Jogador", isMyTeam: true); + + // 4. Registar a equipa adversária + for (int i = 0; i < oppPlayers.length; i++) { + _registerPlayer( + name: oppPlayers[i]['name'].toString(), + number: oppPlayers[i]['number']?.toString() ?? "0", + dbId: oppPlayers[i]['id'].toString(), // Guarda o UUID real + isMyTeam: false, + isCourt: i < 5 + ); + } + _padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false); + + isLoading = false; + onUpdate(); + } catch (e) { + debugPrint("Erro ao carregar jogadores: $e"); + _padTeam(myCourt, myBench, "Falha", isMyTeam: true); + _padTeam(oppCourt, oppBench, "Falha Opp", isMyTeam: false); + isLoading = false; + onUpdate(); + } + } + + void _registerPlayer({required String name, required String number, String? dbId, required bool isMyTeam, required bool isCourt}) { + if (playerNumbers.containsKey(name)) name = "$name (Opp)"; + + playerNumbers[name] = number; + if (dbId != null) playerDbIds[name] = dbId; // Só guarda na lista de IDs se for um jogador real da BD + + playerStats[name] = {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}; + + if (isMyTeam) { + if (isCourt) myCourt.add(name); else myBench.add(name); + } else { + if (isCourt) oppCourt.add(name); else oppBench.add(name); + } + } + + void _padTeam(List court, List bench, String prefix, {required bool isMyTeam}) { + while (court.length < 5) { + _registerPlayer(name: "Sem $prefix ${court.length + 1}", number: "0", dbId: null, isMyTeam: isMyTeam, isCourt: true); + } + } + + // --- TEMPO E TIMEOUTS --- + // (Mantive o teu código original igualzinho aqui) + void toggleTimer(BuildContext context) { + if (isRunning) { + timer?.cancel(); + } else { + timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (duration.inSeconds > 0) { + duration -= const Duration(seconds: 1); + } else { + timer.cancel(); + isRunning = false; + if (currentQuarter < 4) { + currentQuarter++; + duration = const Duration(minutes: 10); + myFouls = 0; + opponentFouls = 0; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas resetadas!'), backgroundColor: Colors.blue)); + } else { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO!'), backgroundColor: Colors.red)); + } + } + onUpdate(); + }); + } + isRunning = !isRunning; + onUpdate(); + } + + void useTimeout(bool isOpponent) { + if (isOpponent) { + if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++; + } else { + if (myTimeoutsUsed < 3) myTimeoutsUsed++; + } + isRunning = false; + timer?.cancel(); + onUpdate(); + } + + String formatTime() => "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}"; + + // --- LÓGICA DE JOGO --- + void handleActionDrag(BuildContext context, String action, String playerData) { + String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); + final stats = playerStats[name]!; + + if (stats["fls"]! >= 5 && action != "sub_foul") { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $name atingiu 5 faltas e está expulso!'), backgroundColor: Colors.red)); + return; + } + + if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") { + pendingAction = action; + pendingPlayer = playerData; + isSelectingShotLocation = true; + } else { + commitStat(action, playerData); + } + onUpdate(); + } + + void handleSubbing(BuildContext context, String action, String courtPlayerName, bool isOpponent) { + if (action.startsWith("bench_my_") && !isOpponent) { + String benchPlayer = action.replaceAll("bench_my_", ""); + if (playerStats[benchPlayer]!["fls"]! >= 5) return; + int courtIndex = myCourt.indexOf(courtPlayerName); + int benchIndex = myBench.indexOf(benchPlayer); + myCourt[courtIndex] = benchPlayer; + myBench[benchIndex] = courtPlayerName; + showMyBench = false; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer'))); + } + if (action.startsWith("bench_opp_") && isOpponent) { + String benchPlayer = action.replaceAll("bench_opp_", ""); + if (playerStats[benchPlayer]!["fls"]! >= 5) return; + int courtIndex = oppCourt.indexOf(courtPlayerName); + int benchIndex = oppBench.indexOf(benchPlayer); + oppCourt[courtIndex] = benchPlayer; + oppBench[benchIndex] = courtPlayerName; + showOppBench = false; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer'))); + } + onUpdate(); + } + + void registerShotLocation(Offset position) { + bool isMake = pendingAction!.startsWith("add_pts_"); + matchShots.add(ShotRecord(position, isMake)); + commitStat(pendingAction!, pendingPlayer!); + isSelectingShotLocation = false; + pendingAction = null; + pendingPlayer = null; + onUpdate(); + } + + void cancelShotLocation() { + isSelectingShotLocation = false; + pendingAction = null; + pendingPlayer = null; + onUpdate(); + } + + void commitStat(String action, String playerData) { + bool isOpponent = playerData.startsWith("player_opp_"); + String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); + final stats = playerStats[name]!; + + if (action.startsWith("add_pts_")) { + int pts = int.parse(action.split("_").last); + if (isOpponent) opponentScore += pts; else myScore += pts; + stats["pts"] = stats["pts"]! + pts; + if (pts == 2 || pts == 3) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; } + } + else if (action.startsWith("sub_pts_")) { + int pts = int.parse(action.split("_").last); + if (isOpponent) { opponentScore = (opponentScore - pts < 0) ? 0 : opponentScore - pts; } + else { myScore = (myScore - pts < 0) ? 0 : myScore - pts; } + stats["pts"] = (stats["pts"]! - pts < 0) ? 0 : stats["pts"]! - pts; + if (pts == 2 || pts == 3) { + if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1; + if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1; + } + } + else if (action == "miss_2" || action == "miss_3") { stats["fga"] = stats["fga"]! + 1; } + else if (action == "add_rbs") { stats["rbs"] = stats["rbs"]! + 1; } + else if (action == "add_ast") { stats["ast"] = stats["ast"]! + 1; } + else if (action == "add_stl") { stats["stl"] = stats["stl"]! + 1; } + else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; } + else if (action == "add_blk") { stats["blk"] = stats["blk"]! + 1; } + else if (action == "add_foul") { + stats["fls"] = stats["fls"]! + 1; + if (isOpponent) { opponentFouls++; } else { myFouls++; } + } + 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--; } + } + } + + // --- 💾 FUNÇÃO PARA GUARDAR DADOS NA BD --- + Future saveGameStats(BuildContext context) async { + final supabase = Supabase.instance.client; + + isSaving = true; + onUpdate(); + + try { + // 1. Atualizar o resultado final na tabela 'games' + await supabase.from('games').update({ + 'my_score': myScore, + 'opponent_score': opponentScore, + 'status': currentQuarter >= 4 && duration.inSeconds == 0 ? 'Terminado' : 'Pausado', + }).eq('id', gameId); + + // 2. Preparar a lista de estatísticas individuais + List> batchStats = []; + + playerStats.forEach((playerName, stats) { + String? memberDbId = playerDbIds[playerName]; // Vai buscar o UUID real do jogador + + // Só guarda se for um jogador real (com ID) e se tiver feito ALGUMA coisa (pontos, faltas, etc) + if (memberDbId != null && stats.values.any((val) => val > 0)) { + bool isMyTeamPlayer = myCourt.contains(playerName) || myBench.contains(playerName); + String teamId = isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!; + + batchStats.add({ + 'game_id': gameId, + 'member_id': memberDbId, + 'team_id': teamId, + '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'], + }); + } + }); + + // 3. Apagar stats antigas deste jogo para não haver duplicados caso cliques no botão "Guardar" 2 vezes + await supabase.from('player_stats').delete().eq('game_id', gameId); + + // 4. Inserir as novas estatísticas de todos os jogadores de uma vez + if (batchStats.isNotEmpty) { + await supabase.from('player_stats').insert(batchStats); + } + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas guardadas 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; + onUpdate(); + } + } + + void dispose() { + timer?.cancel(); + } +} \ No newline at end of file diff --git a/lib/models/game_model.dart b/lib/models/game_model.dart index 1d836b2..64f2f79 100644 --- a/lib/models/game_model.dart +++ b/lib/models/game_model.dart @@ -2,37 +2,37 @@ class Game { final String id; final String myTeam; final String opponentTeam; + final String? myTeamLogo; // URL da imagem + final String? opponentTeamLogo; // URL da imagem final String myScore; final String opponentScore; - final String season; final String status; - final DateTime date; + final String season; Game({ required this.id, required this.myTeam, required this.opponentTeam, + this.myTeamLogo, + this.opponentTeamLogo, required this.myScore, required this.opponentScore, - required this.season, required this.status, - required this.date, + required this.season, }); - // Converte dados do Supabase para o Objeto Dart + // No seu factory, certifique-se de mapear os campos da tabela (ou de um JOIN) factory Game.fromMap(Map map) { return Game( - id: map['id'] ?? '', - myTeam: map['my_team'] ?? 'Desconhecido', - opponentTeam: map['opponent_team'] ?? 'Desconhecido', - // Convertemos para String porque no DB é Integer, mas na UI usas String - myScore: (map['my_score'] ?? 0).toString(), - opponentScore: (map['opponent_score'] ?? 0).toString(), - season: map['season'] ?? '', - status: map['status'] ?? 'Brevemente', - date: map['game_date'] != null - ? DateTime.parse(map['game_date']) - : DateTime.now(), + id: map['id'], + myTeam: map['my_team_name'], + opponentTeam: map['opponent_team_name'], + myTeamLogo: map['my_team_logo'], // Certifique-se que o Supabase retorna isto + opponentTeamLogo: map['opponent_team_logo'], + myScore: map['my_score'].toString(), + opponentScore: map['opponent_score'].toString(), + status: map['status'], + season: map['season'], ); } } \ No newline at end of file diff --git a/lib/pages/PlacarPage.dart b/lib/pages/PlacarPage.dart index f542ab7..8d701d5 100644 --- a/lib/pages/PlacarPage.dart +++ b/lib/pages/PlacarPage.dart @@ -1,13 +1,7 @@ -import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -// NOVA CLASSE PARA GUARDAR OS LANÇAMENTOS NO CAMPO -class ShotRecord { - final Offset position; - final bool isMake; - ShotRecord(this.position, this.isMake); -} +import 'package:playmaker/controllers/placar_controller.dart'; +import 'package:playmaker/widgets/placar_widgets.dart'; class PlacarPage extends StatefulWidget { final String gameId, myTeam, opponentTeam; @@ -18,222 +12,66 @@ class PlacarPage extends StatefulWidget { } class _PlacarPageState extends State { - int _myScore = 0; - int _opponentScore = 0; - int _myFouls = 0; - int _opponentFouls = 0; - int _currentQuarter = 1; - - int _myTimeoutsUsed = 0; - int _opponentTimeoutsUsed = 0; - - List _myCourt = ["Russell", "Reaves", "Davis", "James", "Hachimura"]; - List _myBench = ["Reddish", "Wood", "Hayes", "Prince", "Christie"]; - - List _oppCourt = ["Kyle", "Serge", "Kawhi", "Danny", "Fred"]; - List _oppBench = ["Gasol", "Ibaka", "Siakam", "Lowry", "Powell"]; - - bool _showMyBench = false; - bool _showOppBench = false; - - // --- VARIÁVEIS PARA O MAPA DE LANÇAMENTOS --- - bool _isSelectingShotLocation = false; - String? _pendingAction; - String? _pendingPlayer; - List _matchShots = []; // Guarda as marcas na quadra - - final Map _playerNumbers = { - "Russell": "1", "Reaves": "15", "Davis": "3", "James": "6", "Hachimura": "28", - "Reddish": "5", "Wood": "35", "Hayes": "11", "Prince": "12", "Christie": "10", - "Kyle": "7", "Serge": "9", "Kawhi": "2", "Danny": "14", "Fred": "23", - "Gasol": "33", "Ibaka": "25", "Siakam": "43", "Lowry": "7", "Powell": "24", - }; - - final Map> _playerStats = { - "Russell": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Reaves": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Davis": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "James": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Hachimura": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Reddish": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Wood": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Hayes": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Prince": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Christie": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Kyle": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Serge": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Kawhi": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Danny": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Fred": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Gasol": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Ibaka": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Siakam": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Lowry": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - "Powell": {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}, - }; - - Duration _duration = const Duration(minutes: 10); - Timer? _timer; - bool _isRunning = false; + late PlacarController _controller; @override void initState() { super.initState(); SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeRight, DeviceOrientation.landscapeLeft]); - } - - void _toggleTimer() { - if (_isRunning) { - _timer?.cancel(); - } else { - _timer = Timer.periodic(const Duration(seconds: 1), (timer) { - setState(() { - if (_duration.inSeconds > 0) { - _duration -= const Duration(seconds: 1); - } else { - _timer?.cancel(); - _isRunning = false; - if (_currentQuarter < 4) { - _currentQuarter++; - _duration = const Duration(minutes: 10); - _myFouls = 0; - _opponentFouls = 0; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Período $_currentQuarter iniciado. Faltas de equipa resetadas!'), backgroundColor: Colors.blue), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('FIM DO JOGO!'), backgroundColor: Colors.red), - ); - } - } - }); - }); - } - setState(() => _isRunning = !_isRunning); - } - - void _useTimeout(bool isOpponent) { - setState(() { - if (isOpponent) { - if (_opponentTimeoutsUsed < 3) _opponentTimeoutsUsed++; - } else { - if (_myTimeoutsUsed < 3) _myTimeoutsUsed++; + + _controller = PlacarController( + gameId: widget.gameId, + myTeam: widget.myTeam, + opponentTeam: widget.opponentTeam, + onUpdate: () { + if (mounted) setState(() {}); } - _isRunning = false; - _timer?.cancel(); - }); + ); + _controller.loadPlayers(); } - String _formatTime(Duration d) => - "${d.inMinutes.toString().padLeft(2, '0')}:${d.inSeconds.remainder(60).toString().padLeft(2, '0')}"; - @override void dispose() { - _timer?.cancel(); + _controller.dispose(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); super.dispose(); } - // --- 1. INTERCETAR A AÇÃO PARA VER SE PRECISA DE LOCALIZAÇÃO --- - void _handleActionDrag(String action, String playerData) { - bool isOpponent = playerData.startsWith("player_opp_"); - String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); - final stats = _playerStats[name]!; - - if (stats["fls"]! >= 5 && action != "sub_foul") { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('🛑 $name atingiu 5 faltas e está expulso!'), backgroundColor: Colors.red), - ); - return; - } - - // Se for 2 Pts, 3 Pts, Miss 2 ou Miss 3 -> Abre o mapa para marcar local! - if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") { - setState(() { - _pendingAction = action; - _pendingPlayer = playerData; - _isSelectingShotLocation = true; // Oculta a UI e pede o clique na tela - }); - } else { - // Outras estatísticas (Lances Livres, Ressaltos, Faltas) aplicam direto - _commitStat(action, playerData); - } - } - - // --- 2. SALVAR A POSIÇÃO DO CLIQUE NA QUADRA --- - void _registerShotLocation(Offset position) { - setState(() { - // Guarda a bolinha no mapa (Verde para pts, Vermelha para miss) - bool isMake = _pendingAction!.startsWith("add_pts_"); - _matchShots.add(ShotRecord(position, isMake)); - - // Aplica a estatística de facto - _commitStat(_pendingAction!, _pendingPlayer!); - - // Restaura a tela - _isSelectingShotLocation = false; - _pendingAction = null; - _pendingPlayer = null; - }); - } - - // CANCELAR A ESCOLHA DE LOCALIZAÇÃO (Caso o user se arrependa) - void _cancelShotLocation() { - setState(() { - _isSelectingShotLocation = false; - _pendingAction = null; - _pendingPlayer = null; - }); - } - - // --- 3. APLICAR A ESTATÍSTICA FINAL --- - void _commitStat(String action, String playerData) { - bool isOpponent = playerData.startsWith("player_opp_"); - String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); - final stats = _playerStats[name]!; - - setState(() { - if (action.startsWith("add_pts_")) { - int pts = int.parse(action.split("_").last); - if (isOpponent) _opponentScore += pts; else _myScore += pts; - stats["pts"] = stats["pts"]! + pts; - if (pts == 2 || pts == 3) { - stats["fgm"] = stats["fgm"]! + 1; - stats["fga"] = stats["fga"]! + 1; - } - } - else if (action.startsWith("sub_pts_")) { - int pts = int.parse(action.split("_").last); - if (isOpponent) { _opponentScore = (_opponentScore - pts < 0) ? 0 : _opponentScore - pts; } - else { _myScore = (_myScore - pts < 0) ? 0 : _myScore - pts; } - stats["pts"] = (stats["pts"]! - pts < 0) ? 0 : stats["pts"]! - pts; - if (pts == 2 || pts == 3) { - if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1; - if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1; - } - } - else if (action == "miss_2" || action == "miss_3") { - stats["fga"] = stats["fga"]! + 1; - } - else if (action == "add_rbs") { stats["rbs"] = stats["rbs"]! + 1; } - else if (action == "add_ast") { stats["ast"] = stats["ast"]! + 1; } - else if (action == "add_stl") { stats["stl"] = stats["stl"]! + 1; } - else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; } - else if (action == "add_blk") { stats["blk"] = stats["blk"]! + 1; } - else if (action == "add_foul") { - stats["fls"] = stats["fls"]! + 1; - if (isOpponent) { _opponentFouls++; } else { _myFouls++; } - } - 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--; } - } - }); + // Função auxiliar para criar o botão de arrastar faltas que não está no painel inferior + Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top, double h) { + return Positioned( + top: top, + left: left > 0 ? left : null, + right: right > 0 ? right : null, + child: Draggable( + data: action, + feedback: Material( + color: Colors.transparent, + child: CircleAvatar(radius: 30, backgroundColor: color.withOpacity(0.8), child: Icon(icon, color: Colors.white)), + ), + child: Column( + children: [ + CircleAvatar( + radius: 25, + backgroundColor: color, + child: Icon(icon, color: Colors.white, size: 30), + ), + Text(label, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12)), + ], + ), + ), + ); } @override Widget build(BuildContext context) { + if (_controller.isLoading) { + return const Scaffold( + backgroundColor: Color(0xFF266174), + body: Center(child: CircularProgressIndicator(color: Colors.white)), + ); + } + return Scaffold( backgroundColor: const Color(0xFF266174), body: Stack( @@ -243,633 +81,127 @@ class _PlacarPageState extends State { decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.0)), child: LayoutBuilder( builder: (context, constraints) { - final innerWidth = constraints.maxWidth; - final innerHeight = constraints.maxHeight; + final w = constraints.maxWidth; + final h = constraints.maxHeight; return Stack( children: [ - // GESTURE DETECTOR ABRANGE TODA A QUADRA PARA RECEBER O CLIQUE + // --- MAPA DO CAMPO --- GestureDetector( onTapDown: (details) { - if (_isSelectingShotLocation) { - _registerShotLocation(details.localPosition); - } + if (_controller.isSelectingShotLocation) _controller.registerShotLocation(details.localPosition); }, child: Container( decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/campo.png'), - fit: BoxFit.cover, - alignment: Alignment(0.0, 0.2) - ), + image: DecorationImage(image: AssetImage('assets/campo.png'), fit: BoxFit.cover, alignment: Alignment(0.0, 0.2)), ), - // DESENHA AS BOLINHAS DE LANÇAMENTO NA QUADRA child: Stack( - children: _matchShots.map((shot) => Positioned( - left: shot.position.dx - 8, // Centraliza a bolinha - top: shot.position.dy - 8, - child: CircleAvatar( - radius: 8, - backgroundColor: shot.isMake ? Colors.green : Colors.red, - child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white), - ), + children: _controller.matchShots.map((shot) => Positioned( + left: shot.position.dx - 8, top: shot.position.dy - 8, + child: CircleAvatar(radius: 8, backgroundColor: shot.isMake ? Colors.green : Colors.red, child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white)), )).toList(), ), ), ), - // --- MODO NORMAL DE JOGO --- - if (!_isSelectingShotLocation) ..._buildTacticalFormation(innerWidth, innerHeight), + // --- JOGADORES EM CAMPO --- + if (!_controller.isSelectingShotLocation) ...[ + Positioned(top: h * 0.25, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[0], isOpponent: false)), + Positioned(top: h * 0.68, left: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[1], isOpponent: false)), + Positioned(top: h * 0.45, left: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[2], isOpponent: false)), + Positioned(top: h * 0.15, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[3], isOpponent: false)), + Positioned(top: h * 0.80, left: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.myCourt[4], isOpponent: false)), + + Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[0], isOpponent: true)), + Positioned(top: h * 0.68, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[1], isOpponent: true)), + Positioned(top: h * 0.45, right: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[2], isOpponent: true)), + Positioned(top: h * 0.15, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[3], isOpponent: true)), + Positioned(top: h * 0.80, right: w * 0.20, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[4], isOpponent: true)), + ], - if (!_isSelectingShotLocation) - Positioned( - top: innerHeight * 0.26, left: innerWidth * 0.40, - child: _dragAndTargetBtn("F", Colors.orange, "add_foul", icon: Icons.sports), - ), - if (!_isSelectingShotLocation) - Positioned( - top: innerHeight * 0.26, right: innerWidth * 0.40, - child: _dragAndTargetBtn("F", Colors.orange, "sub_foul", icon: Icons.block), - ), + // --- BOTÕES DE FALTA (FLUTUANTES) --- + // Estes são os botões que você pediu, posicionados em relação ao centro + if (!_controller.isSelectingShotLocation) ...[ + _buildFloatingFoulBtn("FALTA +", Colors.orange, "add_foul", Icons.sports, w * 0.38, 0, h * 0.30, h), + _buildFloatingFoulBtn("FALTA -", Colors.redAccent, "sub_foul", Icons.block, 0, w * 0.38, h * 0.30, h), + ], - if (!_isSelectingShotLocation) + // --- BOTÃO CENTRAL DO TEMPO --- + if (!_controller.isSelectingShotLocation) Positioned( - top: (innerHeight * 0.30) + 70, left: 0, right: 0, + top: (h * 0.30) + 70, left: 0, right: 0, child: Center( child: GestureDetector( - onTap: _toggleTimer, - child: CircleAvatar( - radius: 60, - backgroundColor: Colors.grey.withOpacity(0.5), - child: Icon(_isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 50), - ), + onTap: () => _controller.toggleTimer(context), + child: CircleAvatar(radius: 60, backgroundColor: Colors.grey.withOpacity(0.5), child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 50)), ), ), ), - Positioned(top: 0, left: 0, right: 0, child: Center(child: _buildTopScoreboard())), - - if (!_isSelectingShotLocation) - Positioned(bottom: 10, left: 0, right: 0, child: _buildActionButtonsPanel()), + // --- PLACAR E BOTÕES DE AÇÃO --- + Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller))), + if (!_controller.isSelectingShotLocation) Positioned(bottom: 10, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller)), - // --- MODO SELEÇÃO DE LOCALIZAÇÃO DO LANÇAMENTO --- - if (_isSelectingShotLocation) + // --- OVERLAY DE MARCAÇÃO DE LANÇAMENTO --- + if (_controller.isSelectingShotLocation) Positioned( - top: innerHeight * 0.4, left: 0, right: 0, + top: h * 0.4, left: 0, right: 0, child: Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - color: Colors.black87, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.white) - ), - child: const Text( - "TOQUE NO CAMPO PARA MARCAR O LOCAL DO LANÇAMENTO", - style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), - ), + decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.white)), + child: const Text("TOQUE NO CAMPO PARA MARCAR O LOCAL DO LANÇAMENTO", style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), ), ), ), - - if (_isSelectingShotLocation) - Positioned( - bottom: 20, right: 20, - child: FloatingActionButton.extended( - onPressed: _cancelShotLocation, - backgroundColor: Colors.red, - icon: const Icon(Icons.cancel, color: Colors.white), - label: const Text("Cancelar", style: TextStyle(color: Colors.white)), - ), - ) ], ); }, ), ), - // BOTÕES LATERAIS E BANCO OCULTOS DURANTE A SELEÇÃO DA QUADRA - if (!_isSelectingShotLocation) + // --- MENUS LATERAIS E BANCOS DE SUPLENTES --- + if (!_controller.isSelectingShotLocation) ...[ Positioned( - top: 20, left: 10, + top: 20, left: 10, child: FloatingActionButton( - heroTag: 'btn_save', backgroundColor: const Color(0xFF16202C), mini: true, - onPressed: () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Jogo Guardado!'))), - child: const Icon(Icons.save, color: Colors.white), - ), - ), - if (!_isSelectingShotLocation) - Positioned( - top: 70, left: 10, - child: FloatingActionButton( - heroTag: 'btn_exit', backgroundColor: const Color(0xFFD92C2C), mini: true, - onPressed: () => Navigator.pop(context), - child: const Icon(Icons.exit_to_app, color: Colors.white), - ), + heroTag: 'btn_save', + backgroundColor: const Color(0xFF16202C), + mini: true, + onPressed: _controller.isSaving ? null : () => _controller.saveGameStats(context), + child: _controller.isSaving + ? const SizedBox(width: 15, height: 15, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) + : const Icon(Icons.save, color: Colors.white) + ) ), + Positioned(top: 70, left: 10, child: FloatingActionButton(heroTag: 'btn_exit', backgroundColor: const Color(0xFFD92C2C), mini: true, onPressed: () => Navigator.pop(context), child: const Icon(Icons.exit_to_app, color: Colors.white))), - if (!_isSelectingShotLocation) Positioned( bottom: 50, left: 10, child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (_showMyBench) ..._buildBenchPlayers(_myBench, false), + if (_controller.showMyBench) BenchPlayersList(controller: _controller, isOpponent: false), const SizedBox(height: 10), - FloatingActionButton( - heroTag: 'btn_sub_home', backgroundColor: const Color(0xFF1E5BB2), mini: true, - onPressed: () => setState(() => _showMyBench = !_showMyBench), - child: const Icon(Icons.swap_horiz, color: Colors.white), - ), + FloatingActionButton(heroTag: 'btn_sub_home', backgroundColor: const Color(0xFF1E5BB2), mini: true, onPressed: () { _controller.showMyBench = !_controller.showMyBench; _controller.onUpdate(); }, child: const Icon(Icons.swap_horiz, color: Colors.white)), ], ), ), - if (!_isSelectingShotLocation) Positioned( bottom: 50, right: 10, child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (_showOppBench) ..._buildBenchPlayers(_oppBench, true), + if (_controller.showOppBench) BenchPlayersList(controller: _controller, isOpponent: true), const SizedBox(height: 10), - FloatingActionButton( - heroTag: 'btn_sub_away', backgroundColor: const Color(0xFFD92C2C), mini: true, - onPressed: () => setState(() => _showOppBench = !_showOppBench), - child: const Icon(Icons.swap_horiz, color: Colors.white), - ), + FloatingActionButton(heroTag: 'btn_sub_away', backgroundColor: const Color(0xFFD92C2C), mini: true, onPressed: () { _controller.showOppBench = !_controller.showOppBench; _controller.onUpdate(); }, child: const Icon(Icons.swap_horiz, color: Colors.white)), ], ), ), + ], ], ), ); } - - List _buildTacticalFormation(double w, double h) { - return [ - Positioned(top: h * 0.25, left: w * 0.02, child: _buildPlayerCard(_playerNumbers[_myCourt[0]]!, _myCourt[0], false)), - Positioned(top: h * 0.68, left: w * 0.02, child: _buildPlayerCard(_playerNumbers[_myCourt[1]]!, _myCourt[1], false)), - Positioned(top: h * 0.45, left: w * 0.25, child: _buildPlayerCard(_playerNumbers[_myCourt[2]]!, _myCourt[2], false)), - Positioned(top: h * 0.15, left: w * 0.20, child: _buildPlayerCard(_playerNumbers[_myCourt[3]]!, _myCourt[3], false)), - Positioned(top: h * 0.80, left: w * 0.20, child: _buildPlayerCard(_playerNumbers[_myCourt[4]]!, _myCourt[4], false)), - - Positioned(top: h * 0.25, right: w * 0.02, child: _buildPlayerCard(_playerNumbers[_oppCourt[0]]!, _oppCourt[0], true)), - Positioned(top: h * 0.68, right: w * 0.02, child: _buildPlayerCard(_playerNumbers[_oppCourt[1]]!, _oppCourt[1], true)), - Positioned(top: h * 0.45, right: w * 0.25, child: _buildPlayerCard(_playerNumbers[_oppCourt[2]]!, _oppCourt[2], true)), - Positioned(top: h * 0.15, right: w * 0.20, child: _buildPlayerCard(_playerNumbers[_oppCourt[3]]!, _oppCourt[3], true)), - Positioned(top: h * 0.80, right: w * 0.20, child: _buildPlayerCard(_playerNumbers[_oppCourt[4]]!, _oppCourt[4], true)), - ]; - } - - List _buildBenchPlayers(List bench, bool isOpponent) { - final teamColor = isOpponent ? const Color(0xFFD92C2C) : const Color(0xFF1E5BB2); - // CORREÇÃO: Utilização do prefixo 'bench_' em vez de 'sub_' - final prefix = isOpponent ? "bench_opp_" : "bench_my_"; - - return bench.map((playerName) { - final num = _playerNumbers[playerName]!; - final int fouls = _playerStats[playerName]!["fls"]!; - final bool isFouledOut = fouls >= 5; - - Widget avatarUI = Container( - margin: const EdgeInsets.only(bottom: 5), - child: CircleAvatar( - backgroundColor: isFouledOut ? Colors.grey.shade700 : teamColor, - child: Text( - num, - style: TextStyle( - color: isFouledOut ? Colors.red.shade300 : Colors.white, - fontSize: 14, - decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none - ) - ), - ), - ); - - if (isFouledOut) { - return GestureDetector( - onTap: () => ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('🛑 $playerName não pode voltar ao jogo (Expulso com 5 faltas).'), backgroundColor: Colors.red), - ), - child: avatarUI, - ); - } - - return Draggable( - data: "$prefix$playerName", - feedback: Material( - color: Colors.transparent, - child: CircleAvatar(backgroundColor: teamColor, child: Text(num, style: const TextStyle(color: Colors.white))), - ), - childWhenDragging: const Opacity(opacity: 0.5, child: SizedBox(width: 40, height: 40)), - child: avatarUI, - ); - }).toList(); - } - - Widget _buildPlayerCard(String number, String name, bool isOpponent) { - final teamColor = isOpponent ? const Color(0xFFD92C2C) : const Color(0xFF1E5BB2); - final stats = _playerStats[name]!; - final prefix = isOpponent ? "player_opp_" : "player_my_"; - - return Draggable( - data: "$prefix$name", - feedback: Material( - color: Colors.transparent, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(8)), - child: Text(name, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), - ), - ), - childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, name, stats, teamColor, false, false)), - child: DragTarget( - onAcceptWithDetails: (details) { - final action = details.data; - - if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) { - _handleActionDrag(action, "$prefix$name"); - } - // CORREÇÃO: Nova lógica que processa apenas ações que comecem por 'bench_' para substituições - else if (action.startsWith("bench_")) { - setState(() { - if (action.startsWith("bench_my_") && !isOpponent) { - String benchPlayer = action.replaceAll("bench_my_", ""); - if (_playerStats[benchPlayer]!["fls"]! >= 5) return; - - int courtIndex = _myCourt.indexOf(name); - int benchIndex = _myBench.indexOf(benchPlayer); - _myCourt[courtIndex] = benchPlayer; - _myBench[benchIndex] = name; - _showMyBench = false; - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $name, Entra $benchPlayer'))); - } - if (action.startsWith("bench_opp_") && isOpponent) { - String benchPlayer = action.replaceAll("bench_opp_", ""); - if (_playerStats[benchPlayer]!["fls"]! >= 5) return; - - int courtIndex = _oppCourt.indexOf(name); - int benchIndex = _oppBench.indexOf(benchPlayer); - _oppCourt[courtIndex] = benchPlayer; - _oppBench[benchIndex] = name; - _showOppBench = false; - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $name, Entra $benchPlayer'))); - } - }); - } - }, - builder: (context, candidateData, rejectedData) { - // CORREÇÃO: Atualização da verificação de hover com base no novo prefixo - 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, name, stats, teamColor, isSubbing, isActionHover); - }, - ), - ); - } - - Widget _playerCardUI(String number, String name, Map stats, Color teamColor, bool isSubbing, bool isActionHover) { - bool isFouledOut = stats["fls"]! >= 5; - - Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white; - Color borderColor = isFouledOut ? Colors.redAccent : Colors.transparent; - - if (isSubbing) { - bgColor = Colors.blue.shade50; - borderColor = Colors.blue; - } else if (isActionHover && !isFouledOut) { - bgColor = Colors.orange.shade50; - borderColor = Colors.orange; - } - - int fgm = stats["fgm"]!; - int fga = stats["fga"]!; - String fgPercent = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) : "0"; - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: borderColor, width: 2), - boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 3))], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 40, height: 40, - decoration: BoxDecoration(color: isFouledOut ? Colors.grey : teamColor, borderRadius: BorderRadius.circular(8)), - alignment: Alignment.center, - child: Text(number, style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold)), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: isFouledOut ? Colors.red : Colors.black87, - decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none - ) - ), - const SizedBox(height: 1), - Text( - "${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", - style: TextStyle(fontSize: 11, color: isFouledOut ? Colors.red : Colors.grey[700], fontWeight: FontWeight.w600) - ), - Text( - "${stats["ast"]} Ast | ${stats["rbs"]} Rbs | ${stats["fls"]} Fls", - style: TextStyle(fontSize: 11, color: isFouledOut ? Colors.red : Colors.grey, fontWeight: FontWeight.w500) - ), - ], - ), - ], - ), - ); - } - - Widget _buildTopScoreboard() { - return Container( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 30), - decoration: BoxDecoration( - color: const Color(0xFF16202C), - borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(15), bottomRight: Radius.circular(15)), - border: Border.all(color: Colors.white, width: 2), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildTeamSection(widget.myTeam, _myScore, _myFouls, _myTimeoutsUsed, const Color(0xFF1E5BB2), false), - const SizedBox(width: 25), - Column( - children: [ - _timeDisplay(), - const SizedBox(height: 5), - Text("PERÍODO $_currentQuarter", style: const TextStyle(color: Colors.orangeAccent, fontSize: 14, fontWeight: FontWeight.bold)), - ], - ), - const SizedBox(width: 25), - _buildTeamSection(widget.opponentTeam, _opponentScore, _opponentFouls, _opponentTimeoutsUsed, const Color(0xFFD92C2C), true), - ], - ), - ); - } - - Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp) { - final timeoutIndicators = Row( - mainAxisSize: MainAxisSize.min, - children: List.generate(3, (index) => Container( - margin: const EdgeInsets.symmetric(horizontal: 3), - width: 12, height: 12, - decoration: BoxDecoration(shape: BoxShape.circle, color: index < timeouts ? Colors.yellow : Colors.grey.shade600, border: Border.all(color: Colors.black26)), - )), - ); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: isOpp - ? [ - Column(children: [_scoreBox(score, color), const SizedBox(height: 4), Text("FALTAS: $fouls", style: TextStyle(color: fouls >= 5 ? Colors.red : Colors.yellowAccent, fontSize: 12, fontWeight: FontWeight.bold)), timeoutIndicators]), - const SizedBox(width: 15), - Text(name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)) - ] - : [ - Text(name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), - const SizedBox(width: 15), - Column(children: [_scoreBox(score, color), const SizedBox(height: 4), Text("FALTAS: $fouls", style: TextStyle(color: fouls >= 5 ? Colors.red : Colors.yellowAccent, fontSize: 12, fontWeight: FontWeight.bold)), timeoutIndicators]) - ] - ); - } - - Widget _timeDisplay() => Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6), - decoration: BoxDecoration(color: const Color(0xFF2C3E50), borderRadius: BorderRadius.circular(6)), - child: Text(_formatTime(_duration), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, fontFamily: 'monospace')), - ); - - Widget _scoreBox(int score, Color color) => Container( - width: 50, height: 40, - alignment: Alignment.center, - decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(6)), - child: Text(score.toString(), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)), - ); - - Widget _buildActionButtonsPanel() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // COLUNA 1: 1 Ponto - _columnBtn([ - _actionBtn("T.O", const Color(0xFF1E5BB2), () => _useTimeout(false), labelSize: 20), - _dragAndTargetBtn("1", Colors.orange, "add_pts_1"), - _dragAndTargetBtn("1", Colors.orange, "sub_pts_1", isX: true), - _dragAndTargetBtn("AST", Colors.blueGrey, "add_ast"), - ]), - const SizedBox(width: 15), - - // COLUNA 2: 2 Pontos - _columnBtn([ - _dragAndTargetBtn("M2", Colors.redAccent, "miss_2"), - _dragAndTargetBtn("2", Colors.orange, "add_pts_2"), - _dragAndTargetBtn("2", Colors.orange, "sub_pts_2", isX: true), - _dragAndTargetBtn("STL", Colors.green, "add_stl"), - ]), - const SizedBox(width: 15), - - // COLUNA 3: 3 Pontos - _columnBtn([ - _dragAndTargetBtn("M3", Colors.redAccent, "miss_3"), - _dragAndTargetBtn("3", Colors.orange, "add_pts_3"), - _dragAndTargetBtn("3", Colors.orange, "sub_pts_3", isX: true), - _dragAndTargetBtn("TOV", Colors.redAccent, "add_tov"), - ]), - const SizedBox(width: 15), - - // COLUNA 4: Outras Stats - _columnBtn([ - _actionBtn("T.O", const Color(0xFFD92C2C), () => _useTimeout(true), labelSize: 20), - _dragAndTargetBtn("ORB", const Color(0xFF1E2A38), "add_rbs", icon: Icons.sports_basketball), - _dragAndTargetBtn("DRB", const Color(0xFF1E2A38), "add_rbs", icon: Icons.sports_basketball), - // AQUI ESTÁ O BLK COM ÍCONE DE MÃO - _dragAndTargetBtn("BLK", Colors.deepPurple, "add_blk", icon: Icons.front_hand), - ]), - ], - ); - } - - Widget _columnBtn(List children) => Column(mainAxisSize: MainAxisSize.min, children: children.map((c) => Padding(padding: const EdgeInsets.only(bottom: 8), child: c)).toList()); - - Widget _dragAndTargetBtn(String label, Color color, String actionData, {IconData? icon, bool isX = false}) { - return Draggable( - data: actionData, - feedback: _circle(label, color, icon, true, isX: isX), - childWhenDragging: Opacity(opacity: 0.5, child: _circle(label, color, icon, false, isX: isX)), - child: DragTarget( - onAcceptWithDetails: (details) { - final playerData = details.data; - if (playerData.startsWith("player_")) { - _handleActionDrag(actionData, playerData); - } - }, - builder: (context, candidateData, rejectedData) { - bool isHovered = candidateData.any((data) => data != null && data.startsWith("player_")); - - return Transform.scale( - scale: isHovered ? 1.15 : 1.0, - child: Container( - decoration: isHovered - ? BoxDecoration(shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.white, blurRadius: 10, spreadRadius: 3)]) - : null, - child: _circle(label, color, icon, false, isX: isX) - ), - ); - } - ), - ); - } - - Widget _actionBtn(String label, Color color, VoidCallback onTap, {IconData? icon, bool isX = false, double labelSize = 24}) { - return GestureDetector(onTap: onTap, child: _circle(label, color, icon, false, fontSize: labelSize, isX: isX)); - } - -Widget _circle(String label, Color color, IconData? icon, bool isFeed, {double fontSize = 20, bool isX = false}) { - Widget content; - bool isPointBtn = label == "1" || label == "2" || label == "3"; - bool isMissBtn = label == "M2" || label == "M3"; - bool isBlkBtn = label == "BLK"; - - // --- DESIGN SIMPLIFICADO: BOLA COM LINHAS PRETAS E NÚMERO --- - if (isPointBtn || isMissBtn) { - content = Stack( - alignment: Alignment.center, - children: [ - // 1. CÍRCULO SÓLIDO PRETO: Isto preenche as partes "transparentes" do ícone com preto! - Container( - width: isFeed ? 55 : 45, // Tamanho exato para não vazar pelas bordas da bola - height: isFeed ? 55 : 45, - decoration: const BoxDecoration( - color: Colors.black, // O preto sólido das linhas - shape: BoxShape.circle, - ), - ), - - // 2. Ícone da Bola de Basquete (Laranja para marcar, avermelhado para falhar) - Icon( - Icons.sports_basketball, - color: color, // Usa a cor laranja ou vermelha passada no botão - size: isFeed ? 65 : 55 - ), - - // 3. Número no centro (Preto com contorno branco) - Stack( - children: [ - // Contorno Branco - Text( - label, - style: TextStyle( - fontSize: isFeed ? 26 : 22, - fontWeight: FontWeight.w900, - foreground: Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = 3 - ..color = Colors.white, - decoration: TextDecoration.none, - ), - ), - // Texto Preto - Text( - label, - style: TextStyle( - fontSize: isFeed ? 26 : 22, - fontWeight: FontWeight.w900, - color: Colors.black, - decoration: TextDecoration.none, - ), - ), - ], - ), - ], - ); - } - // --- DESIGN DE MÃO COM TEXTO PARA O BLK --- - else if (isBlkBtn) { - content = Stack( - alignment: Alignment.center, - children: [ - Icon( - Icons.front_hand, - color: const Color.fromARGB(207, 56, 52, 52), - size: isFeed ? 55 : 45 - ), - Stack( - alignment: Alignment.center, - children: [ - Text( - label, - style: TextStyle( - fontSize: isFeed ? 18 : 16, - fontWeight: FontWeight.w900, - foreground: Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = 3 - ..color = Colors.black, - decoration: TextDecoration.none, - ), - ), - Text( - label, - style: TextStyle( - fontSize: isFeed ? 18 : 16, - fontWeight: FontWeight.w900, - color: Colors.white, - decoration: TextDecoration.none, - ), - ), - ], - ), - ], - ); - } - // --- RESTANTES BOTÕES DO SISTEMA --- - else if (icon != null) { - content = Icon(icon, color: Colors.white, size: 30); - } else { - content = Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: fontSize, decoration: TextDecoration.none)); - } - - return Stack( - clipBehavior: Clip.none, - alignment: Alignment.bottomRight, - children: [ - Container( - width: isFeed ? 70 : 60, height: isFeed ? 70 : 60, - decoration: (isPointBtn || isMissBtn || isBlkBtn) - ? const BoxDecoration(color: Colors.transparent) // Retira o círculo de fundo base - : BoxDecoration( - gradient: RadialGradient(colors: [color.withOpacity(0.7), color], radius: 0.8), - shape: BoxShape.circle, - boxShadow: const [BoxShadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 3))] - ), - alignment: Alignment.center, - child: content, - ), - // Ícone de Anular - if (isX) - Positioned( - top: 0, - right: 0, - child: Container( - decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), - child: Icon(Icons.cancel, color: Colors.red, size: isFeed ? 28 : 24) - ), - ), - ], - ); - } } \ No newline at end of file diff --git a/lib/pages/gamePage.dart b/lib/pages/gamePage.dart index 613ae86..ec67834 100644 --- a/lib/pages/gamePage.dart +++ b/lib/pages/gamePage.dart @@ -24,36 +24,60 @@ class _GamePageState extends State { backgroundColor: Colors.white, elevation: 0, ), - body: StreamBuilder>( - // LÊ DIRETAMENTE DO SUPABASE - stream: gameController.gamesStream, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - if (snapshot.hasError) { - return Center(child: Text("Erro: ${snapshot.error}")); - } + // 1º STREAM: Lemos as equipas para ter as imagens + body: StreamBuilder>>( + stream: teamController.teamsStream, + builder: (context, teamSnapshot) { + final List> teamsList = teamSnapshot.data ?? []; - if (!snapshot.hasData || snapshot.data!.isEmpty) { - return const Center(child: Text("Nenhum jogo registado.")); - } - - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: snapshot.data!.length, - itemBuilder: (context, index) { - final game = snapshot.data![index]; + // 2º STREAM: Lemos os jogos + return StreamBuilder>( + stream: gameController.gamesStream, + builder: (context, gameSnapshot) { + if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } - return GameResultCard( - gameId: game.id, - myTeam: game.myTeam, - opponentTeam: game.opponentTeam, - myScore: game.myScore, - opponentScore: game.opponentScore, - status: game.status, - season: game.season, + if (gameSnapshot.hasError) { + return Center(child: Text("Erro: ${gameSnapshot.error}")); + } + + if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) { + return const Center(child: Text("Nenhum jogo registado.")); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: gameSnapshot.data!.length, + itemBuilder: (context, index) { + final game = gameSnapshot.data![index]; + + // --- LÓGICA PARA ENCONTRAR A IMAGEM PELO NOME --- + String? myLogo; + String? oppLogo; + + for (var team in teamsList) { + if (team['name'] == game.myTeam) { + myLogo = team['image_url']; + } + if (team['name'] == game.opponentTeam) { + oppLogo = team['image_url']; + } + } + + // Agora já passamos as imagens para o cartão! + return GameResultCard( + gameId: game.id, + myTeam: game.myTeam, + opponentTeam: game.opponentTeam, + myScore: game.myScore, + opponentScore: game.opponentScore, + status: game.status, + season: game.season, + myTeamLogo: myLogo, // <-- IMAGEM DA TUA EQUIPA + opponentTeamLogo: oppLogo, // <-- IMAGEM DA EQUIPA ADVERSÁRIA + ); + }, ); }, ); @@ -72,7 +96,7 @@ class _GamePageState extends State { context: context, builder: (context) => CreateGameDialogManual( teamController: teamController, - gameController: gameController, // Passamos o controller para fazer o insert + gameController: gameController, ), ); } diff --git a/lib/screens/team_stats_page.dart b/lib/screens/team_stats_page.dart index 4fdb314..9102443 100644 --- a/lib/screens/team_stats_page.dart +++ b/lib/screens/team_stats_page.dart @@ -4,10 +4,6 @@ import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/team_model.dart'; import '../models/person_model.dart'; -// ========================================== -// 1. WIDGETS -// ========================================== - // --- CABEÇALHO --- class StatsHeader extends StatelessWidget { final Team team; @@ -398,7 +394,7 @@ class StatsController { if (ctx.mounted) { String errorMsg = "Erro ao guardar: $e"; if (e.toString().contains('unique')) { - errorMsg = "Já existe um membro com este nome na equipa."; + errorMsg = "Já existe um membro com este numero na equipa."; } ScaffoldMessenger.of(ctx).showSnackBar( SnackBar(content: Text(errorMsg), backgroundColor: Colors.red) diff --git a/lib/widgets/game_widgets.dart b/lib/widgets/game_widgets.dart index 9ea61fd..b256057 100644 --- a/lib/widgets/game_widgets.dart +++ b/lib/widgets/game_widgets.dart @@ -3,10 +3,12 @@ import 'package:playmaker/pages/PlacarPage.dart'; // Garante que o import está import '../controllers/team_controller.dart'; import '../controllers/game_controller.dart'; -// --- CARD DE EXIBIÇÃO DO JOGO (Mantém-se quase igual) --- +// --- CARD DE EXIBIÇÃO DO JOGO --- class GameResultCard extends StatelessWidget { final String gameId; final String myTeam, opponentTeam, myScore, opponentScore, status, season; + final String? myTeamLogo; // NOVA VARIÁVEL + final String? opponentTeamLogo; // NOVA VARIÁVEL const GameResultCard({ super.key, @@ -17,6 +19,8 @@ class GameResultCard extends StatelessWidget { required this.opponentScore, required this.status, required this.season, + this.myTeamLogo, // ADICIONADO AO CONSTRUTOR + this.opponentTeamLogo, // ADICIONADO AO CONSTRUTOR }); @override @@ -32,18 +36,29 @@ class GameResultCard extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C))), + // Passamos a imagem para a função + Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo)), _buildScoreCenter(context, gameId), - Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87)), + // Passamos a imagem para a função + Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo)), ], ), ); } - Widget _buildTeamInfo(String name, Color color) { + // ATUALIZADO para desenhar a imagem + Widget _buildTeamInfo(String name, Color color, String? logoUrl) { return Column( children: [ - CircleAvatar(backgroundColor: color, child: const Icon(Icons.shield, color: Colors.white)), + CircleAvatar( + backgroundColor: color, + backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) + ? NetworkImage(logoUrl) + : null, + child: (logoUrl == null || logoUrl.isEmpty) + ? const Icon(Icons.shield, color: Colors.white) + : null, + ), const SizedBox(height: 4), Text(name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), @@ -102,10 +117,10 @@ class GameResultCard extends StatelessWidget { ); } -// --- POPUP DE CRIAÇÃO (MODIFICADO PARA SUPABASE) --- +// --- POPUP DE CRIAÇÃO --- class CreateGameDialogManual extends StatefulWidget { final TeamController teamController; - final GameController gameController; // Recebemos o controller do jogo + final GameController gameController; const CreateGameDialogManual({ super.key, @@ -122,7 +137,7 @@ class _CreateGameDialogManualState extends State { final TextEditingController _myTeamController = TextEditingController(); final TextEditingController _opponentController = TextEditingController(); - bool _isLoading = false; // Para mostrar loading no botão + bool _isLoading = false; @override void initState() { @@ -150,7 +165,6 @@ class _CreateGameDialogManualState extends State { ), const SizedBox(height: 15), - // Usamos Autocomplete para equipas (Assumindo que TeamController já é Supabase) _buildSearch(label: "Minha Equipa", controller: _myTeamController), const Padding(padding: EdgeInsets.symmetric(vertical: 8), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey))), @@ -171,7 +185,6 @@ class _CreateGameDialogManualState extends State { if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) { setState(() => _isLoading = true); - // 1. CRIAR NO SUPABASE E OBTER O ID REAL String? newGameId = await widget.gameController.createGame( _myTeamController.text, _opponentController.text, @@ -181,10 +194,8 @@ class _CreateGameDialogManualState extends State { setState(() => _isLoading = false); if (newGameId != null && context.mounted) { - // 2. Fechar Popup Navigator.pop(context); - // 3. Ir para o Placar com o ID real do banco Navigator.push( context, MaterialPageRoute( @@ -206,29 +217,70 @@ class _CreateGameDialogManualState extends State { ); } - + // ATUALIZADO para usar Map e mostrar a imagem na lista de pesquisa Widget _buildSearch({required String label, required TextEditingController controller}) { return StreamBuilder>>( stream: widget.teamController.teamsStream, builder: (context, snapshot) { - List teamList = snapshot.hasData - ? snapshot.data!.map((t) => t['name'].toString()).toList() - : []; + List> teamList = snapshot.hasData ? snapshot.data! : []; - return Autocomplete( - optionsBuilder: (val) { - if (val.text.isEmpty) return const Iterable.empty(); - return teamList.where((t) => t.toLowerCase().contains(val.text.toLowerCase())); + return Autocomplete>( + displayStringForOption: (Map option) => option['name'].toString(), + + optionsBuilder: (TextEditingValue val) { + if (val.text.isEmpty) return const Iterable>.empty(); + return teamList.where((t) => + t['name'].toString().toLowerCase().contains(val.text.toLowerCase())); }, - onSelected: (String selection) { - controller.text = selection; + + onSelected: (Map selection) { + controller.text = selection['name'].toString(); }, + + optionsViewBuilder: (context, onSelected, options) { + return Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4.0, + borderRadius: BorderRadius.circular(8), + child: ConstrainedBox( + // Ajuste do tamanho máximo do pop-up de sugestões + constraints: BoxConstraints(maxHeight: 250, maxWidth: MediaQuery.of(context).size.width * 0.7), + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final option = options.elementAt(index); + final String name = option['name'].toString(); + final String? imageUrl = option['image_url']; + + return ListTile( + leading: CircleAvatar( + backgroundColor: Colors.grey.shade200, + backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) + ? NetworkImage(imageUrl) + : null, + child: (imageUrl == null || imageUrl.isEmpty) + ? const Icon(Icons.shield, color: Colors.grey) + : null, + ), + title: Text(name, style: const TextStyle(fontWeight: FontWeight.bold)), + onTap: () { + onSelected(option); + }, + ); + }, + ), + ), + ), + ); + }, + fieldViewBuilder: (ctx, txtCtrl, node, submit) { - // Sincronizar o controller interno do Autocomplete com o nosso controller externo if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) { txtCtrl.text = controller.text; } - // Importante: Guardar o valor escrito mesmo que não selecionado da lista txtCtrl.addListener(() { controller.text = txtCtrl.text; }); diff --git a/lib/widgets/placar_widgets.dart b/lib/widgets/placar_widgets.dart new file mode 100644 index 0000000..8e75a03 --- /dev/null +++ b/lib/widgets/placar_widgets.dart @@ -0,0 +1,337 @@ +import 'package:flutter/material.dart'; +import 'package:playmaker/controllers/placar_controller.dart'; + +// --- PLACAR SUPERIOR --- +class TopScoreboard extends StatelessWidget { + final PlacarController controller; + const TopScoreboard({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 30), + decoration: BoxDecoration( + color: const Color(0xFF16202C), + borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(15), bottomRight: Radius.circular(15)), + border: Border.all(color: Colors.white, width: 2), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, const Color(0xFF1E5BB2), false), + const SizedBox(width: 25), + Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6), + decoration: BoxDecoration(color: const Color(0xFF2C3E50), borderRadius: BorderRadius.circular(6)), + child: Text(controller.formatTime(), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, fontFamily: 'monospace')), + ), + const SizedBox(height: 5), + Text("PERÍODO ${controller.currentQuarter}", style: const TextStyle(color: Colors.orangeAccent, fontSize: 14, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(width: 25), + _buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, const Color(0xFFD92C2C), true), + ], + ), + ); + } + + Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp) { + final timeoutIndicators = Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(3, (index) => Container( + margin: const EdgeInsets.symmetric(horizontal: 3), + width: 12, height: 12, + decoration: BoxDecoration(shape: BoxShape.circle, color: index < timeouts ? Colors.yellow : Colors.grey.shade600, border: Border.all(color: Colors.black26)), + )), + ); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: isOpp + ? [ + Column(children: [_scoreBox(score, color), const SizedBox(height: 4), Text("FALTAS: $fouls", style: TextStyle(color: fouls >= 5 ? Colors.red : Colors.yellowAccent, fontSize: 12, fontWeight: FontWeight.bold)), timeoutIndicators]), + const SizedBox(width: 15), + Text(name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)) + ] + : [ + Text(name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(width: 15), + Column(children: [_scoreBox(score, color), const SizedBox(height: 4), Text("FALTAS: $fouls", style: TextStyle(color: fouls >= 5 ? Colors.red : Colors.yellowAccent, fontSize: 12, fontWeight: FontWeight.bold)), timeoutIndicators]) + ] + ); + } + + Widget _scoreBox(int score, Color color) => Container( + width: 50, height: 40, + alignment: Alignment.center, + decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(6)), + child: Text(score.toString(), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)), + ); +} + +// --- BANCO DE SUPLENTES --- +class BenchPlayersList extends StatelessWidget { + final PlacarController controller; + final bool isOpponent; + const BenchPlayersList({super.key, required this.controller, required this.isOpponent}); + + @override + Widget build(BuildContext context) { + final bench = isOpponent ? controller.oppBench : controller.myBench; + final teamColor = isOpponent ? const Color(0xFFD92C2C) : const Color(0xFF1E5BB2); + final prefix = isOpponent ? "bench_opp_" : "bench_my_"; + + return Column( + mainAxisSize: MainAxisSize.min, + children: bench.map((playerName) { + final num = controller.playerNumbers[playerName] ?? "0"; + final int fouls = controller.playerStats[playerName]?["fls"] ?? 0; + final bool isFouledOut = fouls >= 5; + + Widget avatarUI = Container( + margin: const EdgeInsets.only(bottom: 5), + child: CircleAvatar( + backgroundColor: isFouledOut ? Colors.grey.shade700 : teamColor, + child: Text(num, style: TextStyle(color: isFouledOut ? Colors.red.shade300 : Colors.white, fontSize: 14, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), + ), + ); + + if (isFouledOut) { + return GestureDetector( + onTap: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $playerName não pode voltar (Expulso).'), backgroundColor: Colors.red)), + child: avatarUI, + ); + } + + return Draggable( + data: "$prefix$playerName", + feedback: Material(color: Colors.transparent, child: CircleAvatar(backgroundColor: teamColor, child: Text(num, style: const TextStyle(color: Colors.white)))), + childWhenDragging: const Opacity(opacity: 0.5, child: SizedBox(width: 40, height: 40)), + child: avatarUI, + ); + }).toList(), + ); + } +} + +// --- CARTÃO DO JOGADOR NO CAMPO --- +class PlayerCourtCard extends StatelessWidget { + final PlacarController controller; + final String name; + final bool isOpponent; + + const PlayerCourtCard({super.key, required this.controller, required this.name, required this.isOpponent}); + + @override + Widget build(BuildContext context) { + final teamColor = isOpponent ? const Color(0xFFD92C2C) : const Color(0xFF1E5BB2); + final stats = controller.playerStats[name]!; + final number = controller.playerNumbers[name]!; + final prefix = isOpponent ? "player_opp_" : "player_my_"; + + return Draggable( + data: "$prefix$name", + feedback: Material( + color: Colors.transparent, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(8)), + child: Text(name, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), + ), + ), + childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, name, stats, teamColor, false, false)), + child: DragTarget( + onAcceptWithDetails: (details) { + final action = details.data; + if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) { + controller.handleActionDrag(context, action, "$prefix$name"); + } + else if (action.startsWith("bench_")) { + controller.handleSubbing(context, action, name, 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, name, stats, teamColor, isSubbing, isActionHover); + }, + ), + ); + } + + Widget _playerCardUI(String number, String name, Map stats, Color teamColor, bool isSubbing, bool isActionHover) { + bool isFouledOut = stats["fls"]! >= 5; + Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white; + Color borderColor = isFouledOut ? Colors.redAccent : Colors.transparent; + + if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = Colors.blue; } + else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = Colors.orange; } + + int fgm = stats["fgm"]!; + int fga = stats["fga"]!; + String fgPercent = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) : "0"; + String displayName = name.length > 12 ? "${name.substring(0, 10)}..." : name; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: bgColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: borderColor, width: 2), + boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 3))], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, height: 40, + decoration: BoxDecoration(color: isFouledOut ? Colors.grey : teamColor, borderRadius: BorderRadius.circular(8)), + alignment: Alignment.center, + child: Text(number, style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold)), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, + children: [ + Text(displayName, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: isFouledOut ? Colors.red : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), + const SizedBox(height: 1), + Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 11, color: isFouledOut ? Colors.red : Colors.grey[700], fontWeight: FontWeight.w600)), + Text("${stats["ast"]} Ast | ${stats["rbs"]} Rbs | ${stats["fls"]} Fls", style: TextStyle(fontSize: 11, color: isFouledOut ? Colors.red : Colors.grey, fontWeight: FontWeight.w500)), + ], + ), + ], + ), + ); + } +} + +// --- PAINEL DE BOTÕES DE AÇÃO --- +class ActionButtonsPanel extends StatelessWidget { + final PlacarController controller; + const ActionButtonsPanel({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _columnBtn([ + _actionBtn("T.O", const Color(0xFF1E5BB2), () => controller.useTimeout(false), labelSize: 20), + _dragAndTargetBtn("1", Colors.orange, "add_pts_1"), + _dragAndTargetBtn("1", Colors.orange, "sub_pts_1", isX: true), + _dragAndTargetBtn("STL", Colors.green, "add_stl"), + + ]), + const SizedBox(width: 15), + _columnBtn([ + _dragAndTargetBtn("M2", Colors.redAccent, "miss_2"), + _dragAndTargetBtn("2", Colors.orange, "add_pts_2"), + _dragAndTargetBtn("2", Colors.orange, "sub_pts_2", isX: true), + _dragAndTargetBtn("AST", Colors.blueGrey, "add_ast"), + ]), + const SizedBox(width: 15), + _columnBtn([ + _dragAndTargetBtn("M3", Colors.redAccent, "miss_3"), + _dragAndTargetBtn("3", Colors.orange, "add_pts_3"), + _dragAndTargetBtn("3", Colors.orange, "sub_pts_3", isX: true), + _dragAndTargetBtn("TOV", Colors.redAccent, "add_tov"), + ]), + const SizedBox(width: 15), + _columnBtn([ + _actionBtn("T.O", const Color(0xFFD92C2C), () => controller.useTimeout(true), labelSize: 20), + _dragAndTargetBtn("ORB", const Color(0xFF1E2A38), "add_rbs", icon: Icons.sports_basketball), + _dragAndTargetBtn("DRB", const Color(0xFF1E2A38), "add_rbs", icon: Icons.sports_basketball), + + _dragAndTargetBtn("BLK", Colors.deepPurple, "add_blk", icon: Icons.front_hand), + ]), + const SizedBox(width: 15), + _columnBtn([ + ]) + ], + ); + } + + // Mantenha os métodos _columnBtn, _dragAndTargetBtn, _actionBtn e _circle exatamente como estão + Widget _columnBtn(List children) => Column(mainAxisSize: MainAxisSize.min, children: children.map((c) => Padding(padding: const EdgeInsets.only(bottom: 8), child: c)).toList()); + + Widget _dragAndTargetBtn(String label, Color color, String actionData, {IconData? icon, bool isX = false}) { + return Draggable( + data: actionData, + feedback: _circle(label, color, icon, true, isX: isX), + childWhenDragging: Opacity(opacity: 0.5, child: _circle(label, color, icon, false, isX: isX)), + child: DragTarget( + onAcceptWithDetails: (details) { + final playerData = details.data; + // Requer um BuildContext, não acessível diretamente no Stateless, então não fazemos nada aqui. + // O target real está no PlayerCourtCard! + }, + builder: (context, candidateData, rejectedData) { + bool isHovered = candidateData.any((data) => data != null && data.startsWith("player_")); + return Transform.scale( + scale: isHovered ? 1.15 : 1.0, + child: Container(decoration: isHovered ? BoxDecoration(shape: BoxShape.circle, boxShadow: const [BoxShadow(color: Colors.white, blurRadius: 10, spreadRadius: 3)]) : null, child: _circle(label, color, icon, false, isX: isX)), + ); + } + ), + ); + } + + Widget _actionBtn(String label, Color color, VoidCallback onTap, {IconData? icon, bool isX = false, double labelSize = 24}) { + return GestureDetector(onTap: onTap, child: _circle(label, color, icon, false, fontSize: labelSize, isX: isX)); + } + + Widget _circle(String label, Color color, IconData? icon, bool isFeed, {double fontSize = 20, bool isX = false}) { + Widget content; + bool isPointBtn = label == "1" || label == "2" || label == "3" || label == "M2" || label == "M3"; + bool isBlkBtn = label == "BLK"; + + if (isPointBtn) { + content = Stack( + alignment: Alignment.center, + children: [ + Container(width: isFeed ? 55 : 45, height: isFeed ? 55 : 45, decoration: const BoxDecoration(color: Colors.black, shape: BoxShape.circle)), + Icon(Icons.sports_basketball, color: color, size: isFeed ? 65 : 55), + Stack( + children: [ + Text(label, style: TextStyle(fontSize: isFeed ? 26 : 22, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = 3..color = Colors.white, decoration: TextDecoration.none)), + Text(label, style: TextStyle(fontSize: isFeed ? 26 : 22, fontWeight: FontWeight.w900, color: Colors.black, decoration: TextDecoration.none)), + ], + ), + ], + ); + } + else if (isBlkBtn) { + content = Stack( + alignment: Alignment.center, + children: [ + Icon(Icons.front_hand, color: const Color.fromARGB(207, 56, 52, 52), size: isFeed ? 55 : 45), + Stack( + alignment: Alignment.center, + children: [ + Text(label, style: TextStyle(fontSize: isFeed ? 18 : 16, fontWeight: FontWeight.w900, foreground: Paint()..style = PaintingStyle.stroke..strokeWidth = 3..color = Colors.black, decoration: TextDecoration.none)), + Text(label, style: TextStyle(fontSize: isFeed ? 18 : 16, fontWeight: FontWeight.w900, color: Colors.white, decoration: TextDecoration.none)), + ], + ), + ], + ); + } else if (icon != null) { + content = Icon(icon, color: Colors.white, size: 30); + } else { + content = Text(label, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: fontSize, decoration: TextDecoration.none)); + } + + return Stack( + clipBehavior: Clip.none, alignment: Alignment.bottomRight, + children: [ + Container( + width: isFeed ? 70 : 60, height: isFeed ? 70 : 60, + decoration: (isPointBtn || isBlkBtn) ? const BoxDecoration(color: Colors.transparent) : BoxDecoration(gradient: RadialGradient(colors: [color.withOpacity(0.7), color], radius: 0.8), shape: BoxShape.circle, boxShadow: const [BoxShadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 3))]), + alignment: Alignment.center, child: content, + ), + if (isX) Positioned(top: 0, right: 0, child: Container(decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), child: Icon(Icons.cancel, color: Colors.red, size: isFeed ? 28 : 24))), + ], + ); + } + +} \ No newline at end of file