quero que ao voltar para o jogo ele esteja igual quando eu sai
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
340
lib/controllers/placar_controller.dart
Normal file
340
lib/controllers/placar_controller.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
337
lib/widgets/placar_widgets.dart
Normal file
337
lib/widgets/placar_widgets.dart
Normal 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))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user