quero que ao voltar para o jogo ele esteja igual quando eu sai

This commit is contained in:
2026-03-04 12:44:27 +00:00
parent af765fc5ab
commit e6b08befc7
8 changed files with 938 additions and 849 deletions

View File

@@ -5,14 +5,22 @@ class GameController {
final _supabase = Supabase.instance.client; final _supabase = Supabase.instance.client;
// 1. LER JOGOS (Stream em Tempo Real) // 1. LER JOGOS (Stream em Tempo Real)
Stream<List<Game>> get gamesStream { Stream<List<Game>> get gamesStream {
return _supabase return _supabase
.from('games') .from('games') // 1. Fica à escuta da tabela original (Garante o Tempo Real!)
.stream(primaryKey: ['id']) .stream(primaryKey: ['id'])
.order('game_date', ascending: false) // Mais recentes primeiro .asyncMap((event) async {
.map((data) => data.map((json) => Game.fromMap(json)).toList()); // 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 // 2. CRIAR JOGO
// Retorna o ID do jogo criado para podermos navegar para o placar // Retorna o ID do jogo criado para podermos navegar para o placar
Future<String?> createGame(String myTeam, String opponent, String season) async { Future<String?> createGame(String myTeam, String opponent, String season) async {

View File

@@ -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<String> myCourt = [];
List<String> myBench = [];
List<String> oppCourt = [];
List<String> oppBench = [];
Map<String, String> playerNumbers = {};
Map<String, Map<String, int>> playerStats = {};
Map<String, String> 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<ShotRecord> matchShots = [];
Duration duration = const Duration(minutes: 10);
Timer? timer;
bool isRunning = false;
// --- INICIALIZAÇÃO E BD ---
Future<void> 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<dynamic> myPlayers = myTeamDbId != null ? await supabase.from('members').select().eq('team_id', myTeamDbId!).eq('type', 'Jogador') : [];
List<dynamic> 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<String> court, List<String> 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<void> 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<Map<String, dynamic>> 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();
}
}

View File

@@ -2,37 +2,37 @@ class Game {
final String id; final String id;
final String myTeam; final String myTeam;
final String opponentTeam; final String opponentTeam;
final String? myTeamLogo; // URL da imagem
final String? opponentTeamLogo; // URL da imagem
final String myScore; final String myScore;
final String opponentScore; final String opponentScore;
final String season;
final String status; final String status;
final DateTime date; final String season;
Game({ Game({
required this.id, required this.id,
required this.myTeam, required this.myTeam,
required this.opponentTeam, required this.opponentTeam,
this.myTeamLogo,
this.opponentTeamLogo,
required this.myScore, required this.myScore,
required this.opponentScore, required this.opponentScore,
required this.season,
required this.status, 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<String, dynamic> map) { factory Game.fromMap(Map<String, dynamic> map) {
return Game( return Game(
id: map['id'] ?? '', id: map['id'],
myTeam: map['my_team'] ?? 'Desconhecido', myTeam: map['my_team_name'],
opponentTeam: map['opponent_team'] ?? 'Desconhecido', opponentTeam: map['opponent_team_name'],
// Convertemos para String porque no DB é Integer, mas na UI usas String myTeamLogo: map['my_team_logo'], // Certifique-se que o Supabase retorna isto
myScore: (map['my_score'] ?? 0).toString(), opponentTeamLogo: map['opponent_team_logo'],
opponentScore: (map['opponent_score'] ?? 0).toString(), myScore: map['my_score'].toString(),
season: map['season'] ?? '', opponentScore: map['opponent_score'].toString(),
status: map['status'] ?? 'Brevemente', status: map['status'],
date: map['game_date'] != null season: map['season'],
? DateTime.parse(map['game_date'])
: DateTime.now(),
); );
} }
} }

View File

@@ -1,13 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:playmaker/controllers/placar_controller.dart';
// NOVA CLASSE PARA GUARDAR OS LANÇAMENTOS NO CAMPO import 'package:playmaker/widgets/placar_widgets.dart';
class ShotRecord {
final Offset position;
final bool isMake;
ShotRecord(this.position, this.isMake);
}
class PlacarPage extends StatefulWidget { class PlacarPage extends StatefulWidget {
final String gameId, myTeam, opponentTeam; final String gameId, myTeam, opponentTeam;
@@ -18,222 +12,66 @@ class PlacarPage extends StatefulWidget {
} }
class _PlacarPageState extends State<PlacarPage> { class _PlacarPageState extends State<PlacarPage> {
int _myScore = 0; late PlacarController _controller;
int _opponentScore = 0;
int _myFouls = 0;
int _opponentFouls = 0;
int _currentQuarter = 1;
int _myTimeoutsUsed = 0;
int _opponentTimeoutsUsed = 0;
List<String> _myCourt = ["Russell", "Reaves", "Davis", "James", "Hachimura"];
List<String> _myBench = ["Reddish", "Wood", "Hayes", "Prince", "Christie"];
List<String> _oppCourt = ["Kyle", "Serge", "Kawhi", "Danny", "Fred"];
List<String> _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<ShotRecord> _matchShots = []; // Guarda as marcas na quadra
final Map<String, String> _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<String, Map<String, int>> _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;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeRight, DeviceOrientation.landscapeLeft]); SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeRight, DeviceOrientation.landscapeLeft]);
}
void _toggleTimer() { _controller = PlacarController(
if (_isRunning) { gameId: widget.gameId,
_timer?.cancel(); myTeam: widget.myTeam,
} else { opponentTeam: widget.opponentTeam,
_timer = Timer.periodic(const Duration(seconds: 1), (timer) { onUpdate: () {
setState(() { if (mounted) 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),
); );
_controller.loadPlayers();
} }
}
});
});
}
setState(() => _isRunning = !_isRunning);
}
void _useTimeout(bool isOpponent) {
setState(() {
if (isOpponent) {
if (_opponentTimeoutsUsed < 3) _opponentTimeoutsUsed++;
} else {
if (_myTimeoutsUsed < 3) _myTimeoutsUsed++;
}
_isRunning = false;
_timer?.cancel();
});
}
String _formatTime(Duration d) =>
"${d.inMinutes.toString().padLeft(2, '0')}:${d.inSeconds.remainder(60).toString().padLeft(2, '0')}";
@override @override
void dispose() { void dispose() {
_timer?.cancel(); _controller.dispose();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
super.dispose(); super.dispose();
} }
// --- 1. INTERCETAR A AÇÃO PARA VER SE PRECISA DE LOCALIZAÇÃO --- // Função auxiliar para criar o botão de arrastar faltas que não está no painel inferior
void _handleActionDrag(String action, String playerData) { Widget _buildFloatingFoulBtn(String label, Color color, String action, IconData icon, double left, double right, double top, double h) {
bool isOpponent = playerData.startsWith("player_opp_"); return Positioned(
String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); top: top,
final stats = _playerStats[name]!; left: left > 0 ? left : null,
right: right > 0 ? right : null,
if (stats["fls"]! >= 5 && action != "sub_foul") { child: Draggable<String>(
ScaffoldMessenger.of(context).showSnackBar( data: action,
SnackBar(content: Text('🛑 $name atingiu 5 faltas e está expulso!'), backgroundColor: Colors.red), 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)),
],
),
),
); );
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--; }
}
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_controller.isLoading) {
return const Scaffold(
backgroundColor: Color(0xFF266174),
body: Center(child: CircularProgressIndicator(color: Colors.white)),
);
}
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFF266174), backgroundColor: const Color(0xFF266174),
body: Stack( body: Stack(
@@ -243,633 +81,127 @@ class _PlacarPageState extends State<PlacarPage> {
decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.0)), decoration: BoxDecoration(border: Border.all(color: Colors.white, width: 2.0)),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final innerWidth = constraints.maxWidth; final w = constraints.maxWidth;
final innerHeight = constraints.maxHeight; final h = constraints.maxHeight;
return Stack( return Stack(
children: [ children: [
// GESTURE DETECTOR ABRANGE TODA A QUADRA PARA RECEBER O CLIQUE // --- MAPA DO CAMPO ---
GestureDetector( GestureDetector(
onTapDown: (details) { onTapDown: (details) {
if (_isSelectingShotLocation) { if (_controller.isSelectingShotLocation) _controller.registerShotLocation(details.localPosition);
_registerShotLocation(details.localPosition);
}
}, },
child: Container( child: Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
image: DecorationImage( image: DecorationImage(image: AssetImage('assets/campo.png'), fit: BoxFit.cover, alignment: Alignment(0.0, 0.2)),
image: AssetImage('assets/campo.png'),
fit: BoxFit.cover,
alignment: Alignment(0.0, 0.2)
), ),
),
// DESENHA AS BOLINHAS DE LANÇAMENTO NA QUADRA
child: Stack( child: Stack(
children: _matchShots.map((shot) => Positioned( children: _controller.matchShots.map((shot) => Positioned(
left: shot.position.dx - 8, // Centraliza a bolinha left: shot.position.dx - 8, top: shot.position.dy - 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)),
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(), )).toList(),
), ),
), ),
), ),
// --- MODO NORMAL DE JOGO --- // --- JOGADORES EM CAMPO ---
if (!_isSelectingShotLocation) ..._buildTacticalFormation(innerWidth, innerHeight), 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)),
if (!_isSelectingShotLocation) Positioned(top: h * 0.25, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[0], isOpponent: true)),
Positioned( Positioned(top: h * 0.68, right: w * 0.02, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[1], isOpponent: true)),
top: innerHeight * 0.26, left: innerWidth * 0.40, Positioned(top: h * 0.45, right: w * 0.25, child: PlayerCourtCard(controller: _controller, name: _controller.oppCourt[2], isOpponent: true)),
child: _dragAndTargetBtn("F", Colors.orange, "add_foul", icon: Icons.sports), 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, right: innerWidth * 0.40,
child: _dragAndTargetBtn("F", Colors.orange, "sub_foul", icon: Icons.block),
),
if (!_isSelectingShotLocation) // --- 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),
],
// --- BOTÃO CENTRAL DO TEMPO ---
if (!_controller.isSelectingShotLocation)
Positioned( Positioned(
top: (innerHeight * 0.30) + 70, left: 0, right: 0, top: (h * 0.30) + 70, left: 0, right: 0,
child: Center( child: Center(
child: GestureDetector( child: GestureDetector(
onTap: _toggleTimer, onTap: () => _controller.toggleTimer(context),
child: CircleAvatar( child: CircleAvatar(radius: 60, backgroundColor: Colors.grey.withOpacity(0.5), child: Icon(_controller.isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 50)),
radius: 60,
backgroundColor: Colors.grey.withOpacity(0.5),
child: Icon(_isRunning ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 50),
),
), ),
), ),
), ),
Positioned(top: 0, left: 0, right: 0, child: Center(child: _buildTopScoreboard())), // --- 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)),
if (!_isSelectingShotLocation) // --- OVERLAY DE MARCAÇÃO DE LANÇAMENTO ---
Positioned(bottom: 10, left: 0, right: 0, child: _buildActionButtonsPanel()), if (_controller.isSelectingShotLocation)
// --- MODO SELEÇÃO DE LOCALIZAÇÃO DO LANÇAMENTO ---
if (_isSelectingShotLocation)
Positioned( Positioned(
top: innerHeight * 0.4, left: 0, right: 0, top: h * 0.4, left: 0, right: 0,
child: Center( child: Center(
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.white)),
color: Colors.black87, child: const Text("TOQUE NO CAMPO PARA MARCAR O LOCAL DO LANÇAMENTO", style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
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 // --- MENUS LATERAIS E BANCOS DE SUPLENTES ---
if (!_isSelectingShotLocation) if (!_controller.isSelectingShotLocation) ...[
Positioned( Positioned(
top: 20, left: 10, top: 20, left: 10,
child: FloatingActionButton( child: FloatingActionButton(
heroTag: 'btn_save', backgroundColor: const Color(0xFF16202C), mini: true, heroTag: 'btn_save',
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Jogo Guardado!'))), backgroundColor: const Color(0xFF16202C),
child: const Icon(Icons.save, color: Colors.white), mini: true,
), onPressed: _controller.isSaving ? null : () => _controller.saveGameStats(context),
), child: _controller.isSaving
if (!_isSelectingShotLocation) ? const SizedBox(width: 15, height: 15, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
Positioned( : const Icon(Icons.save, color: Colors.white)
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),
),
), ),
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( Positioned(
bottom: 50, left: 10, bottom: 50, left: 10,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (_showMyBench) ..._buildBenchPlayers(_myBench, false), if (_controller.showMyBench) BenchPlayersList(controller: _controller, isOpponent: false),
const SizedBox(height: 10), const SizedBox(height: 10),
FloatingActionButton( 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)),
heroTag: 'btn_sub_home', backgroundColor: const Color(0xFF1E5BB2), mini: true,
onPressed: () => setState(() => _showMyBench = !_showMyBench),
child: const Icon(Icons.swap_horiz, color: Colors.white),
),
], ],
), ),
), ),
if (!_isSelectingShotLocation)
Positioned( Positioned(
bottom: 50, right: 10, bottom: 50, right: 10,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (_showOppBench) ..._buildBenchPlayers(_oppBench, true), if (_controller.showOppBench) BenchPlayersList(controller: _controller, isOpponent: true),
const SizedBox(height: 10), const SizedBox(height: 10),
FloatingActionButton( 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)),
heroTag: 'btn_sub_away', backgroundColor: const Color(0xFFD92C2C), mini: true,
onPressed: () => setState(() => _showOppBench = !_showOppBench),
child: const Icon(Icons.swap_horiz, color: Colors.white),
),
], ],
), ),
), ),
], ],
),
);
}
List<Widget> _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<Widget> _buildBenchPlayers(List<String> 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<String>(
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<String>(
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<String>(
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<String, int> 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<Widget> 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<String>(
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<String>(
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)
),
),
],
); );
} }
} }

View File

@@ -24,28 +24,48 @@ class _GamePageState extends State<GamePage> {
backgroundColor: Colors.white, backgroundColor: Colors.white,
elevation: 0, elevation: 0,
), ),
body: StreamBuilder<List<Game>>( // 1º STREAM: Lemos as equipas para ter as imagens
// LÊ DIRETAMENTE DO SUPABASE body: StreamBuilder<List<Map<String, dynamic>>>(
stream: teamController.teamsStream,
builder: (context, teamSnapshot) {
final List<Map<String, dynamic>> teamsList = teamSnapshot.data ?? [];
// 2º STREAM: Lemos os jogos
return StreamBuilder<List<Game>>(
stream: gameController.gamesStream, stream: gameController.gamesStream,
builder: (context, snapshot) { builder: (context, gameSnapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (snapshot.hasError) { if (gameSnapshot.hasError) {
return Center(child: Text("Erro: ${snapshot.error}")); return Center(child: Text("Erro: ${gameSnapshot.error}"));
} }
if (!snapshot.hasData || snapshot.data!.isEmpty) { if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) {
return const Center(child: Text("Nenhum jogo registado.")); return const Center(child: Text("Nenhum jogo registado."));
} }
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
itemCount: snapshot.data!.length, itemCount: gameSnapshot.data!.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final game = snapshot.data![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( return GameResultCard(
gameId: game.id, gameId: game.id,
myTeam: game.myTeam, myTeam: game.myTeam,
@@ -54,6 +74,10 @@ class _GamePageState extends State<GamePage> {
opponentScore: game.opponentScore, opponentScore: game.opponentScore,
status: game.status, status: game.status,
season: game.season, season: game.season,
myTeamLogo: myLogo, // <-- IMAGEM DA TUA EQUIPA
opponentTeamLogo: oppLogo, // <-- IMAGEM DA EQUIPA ADVERSÁRIA
);
},
); );
}, },
); );
@@ -72,7 +96,7 @@ class _GamePageState extends State<GamePage> {
context: context, context: context,
builder: (context) => CreateGameDialogManual( builder: (context) => CreateGameDialogManual(
teamController: teamController, teamController: teamController,
gameController: gameController, // Passamos o controller para fazer o insert gameController: gameController,
), ),
); );
} }

View File

@@ -4,10 +4,6 @@ import 'package:supabase_flutter/supabase_flutter.dart';
import '../models/team_model.dart'; import '../models/team_model.dart';
import '../models/person_model.dart'; import '../models/person_model.dart';
// ==========================================
// 1. WIDGETS
// ==========================================
// --- CABEÇALHO --- // --- CABEÇALHO ---
class StatsHeader extends StatelessWidget { class StatsHeader extends StatelessWidget {
final Team team; final Team team;
@@ -398,7 +394,7 @@ class StatsController {
if (ctx.mounted) { if (ctx.mounted) {
String errorMsg = "Erro ao guardar: $e"; String errorMsg = "Erro ao guardar: $e";
if (e.toString().contains('unique')) { 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( ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(content: Text(errorMsg), backgroundColor: Colors.red) SnackBar(content: Text(errorMsg), backgroundColor: Colors.red)

View File

@@ -3,10 +3,12 @@ import 'package:playmaker/pages/PlacarPage.dart'; // Garante que o import está
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../controllers/game_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 { class GameResultCard extends StatelessWidget {
final String gameId; final String gameId;
final String myTeam, opponentTeam, myScore, opponentScore, status, season; final String myTeam, opponentTeam, myScore, opponentScore, status, season;
final String? myTeamLogo; // NOVA VARIÁVEL
final String? opponentTeamLogo; // NOVA VARIÁVEL
const GameResultCard({ const GameResultCard({
super.key, super.key,
@@ -17,6 +19,8 @@ class GameResultCard extends StatelessWidget {
required this.opponentScore, required this.opponentScore,
required this.status, required this.status,
required this.season, required this.season,
this.myTeamLogo, // ADICIONADO AO CONSTRUTOR
this.opponentTeamLogo, // ADICIONADO AO CONSTRUTOR
}); });
@override @override
@@ -32,18 +36,29 @@ class GameResultCard extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ 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), _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( return Column(
children: [ 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), const SizedBox(height: 4),
Text(name, Text(name,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), 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 { class CreateGameDialogManual extends StatefulWidget {
final TeamController teamController; final TeamController teamController;
final GameController gameController; // Recebemos o controller do jogo final GameController gameController;
const CreateGameDialogManual({ const CreateGameDialogManual({
super.key, super.key,
@@ -122,7 +137,7 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
final TextEditingController _myTeamController = TextEditingController(); final TextEditingController _myTeamController = TextEditingController();
final TextEditingController _opponentController = TextEditingController(); final TextEditingController _opponentController = TextEditingController();
bool _isLoading = false; // Para mostrar loading no botão bool _isLoading = false;
@override @override
void initState() { void initState() {
@@ -150,7 +165,6 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
// Usamos Autocomplete para equipas (Assumindo que TeamController já é Supabase)
_buildSearch(label: "Minha Equipa", controller: _myTeamController), _buildSearch(label: "Minha Equipa", controller: _myTeamController),
const Padding(padding: EdgeInsets.symmetric(vertical: 8), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey))), 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<CreateGameDialogManual> {
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) { if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
setState(() => _isLoading = true); setState(() => _isLoading = true);
// 1. CRIAR NO SUPABASE E OBTER O ID REAL
String? newGameId = await widget.gameController.createGame( String? newGameId = await widget.gameController.createGame(
_myTeamController.text, _myTeamController.text,
_opponentController.text, _opponentController.text,
@@ -181,10 +194,8 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
setState(() => _isLoading = false); setState(() => _isLoading = false);
if (newGameId != null && context.mounted) { if (newGameId != null && context.mounted) {
// 2. Fechar Popup
Navigator.pop(context); Navigator.pop(context);
// 3. Ir para o Placar com o ID real do banco
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -206,29 +217,70 @@ class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
); );
} }
// ATUALIZADO para usar Map e mostrar a imagem na lista de pesquisa
Widget _buildSearch({required String label, required TextEditingController controller}) { Widget _buildSearch({required String label, required TextEditingController controller}) {
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: widget.teamController.teamsStream, stream: widget.teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
List<String> teamList = snapshot.hasData List<Map<String, dynamic>> teamList = snapshot.hasData ? snapshot.data! : [];
? snapshot.data!.map((t) => t['name'].toString()).toList()
: [];
return Autocomplete<String>( return Autocomplete<Map<String, dynamic>>(
optionsBuilder: (val) { displayStringForOption: (Map<String, dynamic> option) => option['name'].toString(),
if (val.text.isEmpty) return const Iterable<String>.empty();
return teamList.where((t) => t.toLowerCase().contains(val.text.toLowerCase())); optionsBuilder: (TextEditingValue val) {
if (val.text.isEmpty) return const Iterable<Map<String, dynamic>>.empty();
return teamList.where((t) =>
t['name'].toString().toLowerCase().contains(val.text.toLowerCase()));
}, },
onSelected: (String selection) {
controller.text = selection; onSelected: (Map<String, dynamic> 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) { fieldViewBuilder: (ctx, txtCtrl, node, submit) {
// Sincronizar o controller interno do Autocomplete com o nosso controller externo
if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) { if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) {
txtCtrl.text = controller.text; txtCtrl.text = controller.text;
} }
// Importante: Guardar o valor escrito mesmo que não selecionado da lista
txtCtrl.addListener(() { txtCtrl.addListener(() {
controller.text = txtCtrl.text; controller.text = txtCtrl.text;
}); });

View File

@@ -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<String>(
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<String>(
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<String>(
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<String, int> 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<Widget> 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<String>(
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<String>(
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))),
],
);
}
}