Files
PlayMaker/lib/widgets/placar_widgets.dart
2026-04-28 17:15:34 +01:00

1543 lines
64 KiB
Dart

import 'package:flutter/material.dart';
import 'dart:math' as math;
import '../classe/theme.dart';
import '../controllers/placar_controller.dart';
import 'package:playmaker/zone_map_dialog.dart';
// ==============================================================================
// WIDGETS COMPONENTIZADOS E POP-UPS
// ==============================================================================
class ActionSubtypeDialog extends StatelessWidget {
final String title;
final Map<String, String> options;
final Function(String) onSelected;
final double sf;
const ActionSubtypeDialog({super.key, required this.title, required this.options, required this.onSelected, required this.sf});
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
elevation: 0,
child: Container(
width: MediaQuery.of(context).size.width * 0.55,
decoration: BoxDecoration(
color: AppTheme.placarDarkSurface,
borderRadius: BorderRadius.circular(12 * sf),
border: Border.all(color: AppTheme.warningAmber, width: 1.5 * sf),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 12 * sf),
decoration: BoxDecoration(
color: AppTheme.placarListCard,
borderRadius: BorderRadius.vertical(top: Radius.circular(10 * sf)),
),
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.center,
child: Text(
title,
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf),
),
),
Align(
alignment: Alignment.centerRight,
child: InkWell(
onTap: () => Navigator.pop(context),
child: Container(
padding: EdgeInsets.all(4 * sf),
decoration: const BoxDecoration(color: Colors.white24, shape: BoxShape.circle),
child: Icon(Icons.close, color: Colors.white, size: 16 * sf),
),
),
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 20 * sf, horizontal: 15 * sf),
child: Wrap(
spacing: 12 * sf,
runSpacing: 15 * sf,
alignment: WrapAlignment.center,
children: options.entries.map((e) => SizedBox(
width: 110 * sf,
height: 60 * sf,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.placarTimerBg,
foregroundColor: Colors.white,
padding: EdgeInsets.all(6 * sf),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8 * sf),
side: BorderSide(color: Colors.white12, width: 1 * sf),
),
),
onPressed: () => onSelected(e.key), // Retorna a chave correta (ex: "tov_3s")
child: Text(
e.value,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12 * sf),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
)).toList(),
),
),
],
),
),
);
}
}
void showFoulVictimDialog(BuildContext context, PlacarController controller, bool isCommitterOpponent, String committerId, String foulType, double sf) {
final victimCourt = isCommitterOpponent ? controller.myCourt : controller.oppCourt;
final prefixCommitter = isCommitterOpponent ? "player_opp_" : "player_my_";
final prefixVictim = isCommitterOpponent ? "player_my_" : "player_opp_";
final victimsColor = isCommitterOpponent ? AppTheme.myTeamBlue : AppTheme.oppTeamRed;
final possibleVictims = victimCourt.where((id) => !id.startsWith("fake_")).toList();
// Função interna para verificar se o jogador tem de sair
void checkFouledOut() {
final fouls = controller.playerStats[committerId]?["fls"] ?? 0;
final isCourt = isCommitterOpponent ? controller.oppCourt.contains(committerId) : controller.myCourt.contains(committerId);
if (fouls >= 5 && isCourt) {
Future.delayed(const Duration(milliseconds: 300), () {
if (!context.mounted) return;
showDialog(
context: context,
barrierDismissible: false, // Obriga a fazer a substituição
builder: (ctx) => SubstitutionDialog(
controller: controller,
isOpponent: isCommitterOpponent,
sf: sf,
forcedStarterId: committerId, // Passamos o jogador que foi expulso
),
);
});
}
}
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => Dialog(
backgroundColor: Colors.transparent,
elevation: 0,
child: Container(
width: MediaQuery.of(context).size.width * 0.60,
decoration: BoxDecoration(
color: AppTheme.placarDarkSurface,
borderRadius: BorderRadius.circular(12 * sf),
border: Border.all(color: AppTheme.warningAmber, width: 1.5 * sf),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 12 * sf),
decoration: BoxDecoration(
color: AppTheme.placarListCard,
borderRadius: BorderRadius.vertical(top: Radius.circular(10 * sf)),
),
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.center,
child: Text(
"Quem sofreu a falta?",
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf),
),
),
Align(
alignment: Alignment.centerRight,
child: InkWell(
onTap: () => Navigator.pop(ctx),
child: Container(
padding: EdgeInsets.all(4 * sf),
decoration: const BoxDecoration(color: Colors.white24, shape: BoxShape.circle),
child: Icon(Icons.close, color: Colors.white, size: 16 * sf),
),
),
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 20 * sf, horizontal: 10 * sf),
child: Column(
children: [
Wrap(
spacing: 10 * sf,
runSpacing: 10 * sf,
alignment: WrapAlignment.center,
children: possibleVictims.map((id) {
final name = controller.playerNames[id] ?? "Desconhecido";
final number = controller.playerNumbers[id] ?? "0";
return InkWell(
onTap: () {
Navigator.pop(ctx);
controller.registerFoul("$prefixCommitter$committerId", foulType, "$prefixVictim$id");
checkFouledOut(); // Verifica 5 faltas!
},
child: Container(
width: 80 * sf,
padding: EdgeInsets.all(6 * sf),
decoration: BoxDecoration(
color: victimsColor.withOpacity(0.2),
border: Border.all(color: victimsColor, width: 1.5 * sf),
borderRadius: BorderRadius.circular(12 * sf),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
backgroundColor: victimsColor,
radius: 16 * sf,
child: Text(number, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * sf)),
),
SizedBox(height: 6 * sf),
Text(name, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 10 * sf), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis),
],
),
),
);
}).toList(),
),
SizedBox(height: 15 * sf),
const Divider(color: Colors.white24),
SizedBox(height: 8 * sf),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.placarTimerBg,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 10 * sf),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8 * sf),
side: BorderSide(color: Colors.white12, width: 1 * sf),
),
),
icon: Icon(Icons.group, color: Colors.white, size: 16 * sf),
label: Text("Equipa / Sem Vítima Específica", style: TextStyle(fontSize: 12 * sf)),
onPressed: () {
Navigator.pop(ctx);
controller.registerFoul("$prefixCommitter$committerId", foulType, "");
checkFouledOut(); // Verifica 5 faltas!
},
)
],
),
),
],
),
),
),
);
}
void showAssistDialog(BuildContext context, PlacarController controller, bool isOpponent, String scorerId, double sf) {
final teamCourt = isOpponent ? controller.oppCourt : controller.myCourt;
final prefix = isOpponent ? "player_opp_" : "player_my_";
final teamColor = isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue;
final possibleAssistants = teamCourt.where((id) => id != scorerId && !id.startsWith("fake_")).toList();
if (possibleAssistants.isEmpty) return;
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => Dialog(
backgroundColor: Colors.transparent,
elevation: 0,
child: Container(
width: MediaQuery.of(context).size.width * 0.55,
decoration: BoxDecoration(
color: AppTheme.placarDarkSurface,
borderRadius: BorderRadius.circular(12 * sf),
border: Border.all(color: AppTheme.warningAmber, width: 1.5 * sf),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 12 * sf),
decoration: BoxDecoration(
color: AppTheme.placarListCard,
borderRadius: BorderRadius.vertical(top: Radius.circular(10 * sf)),
),
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.center,
child: Text(
"Houve Assistência?",
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf),
),
),
Align(
alignment: Alignment.centerRight,
child: InkWell(
onTap: () => Navigator.pop(ctx),
child: Container(
padding: EdgeInsets.all(4 * sf),
decoration: const BoxDecoration(color: Colors.white24, shape: BoxShape.circle),
child: Icon(Icons.close, color: Colors.white, size: 16 * sf),
),
),
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 20 * sf, horizontal: 10 * sf),
child: Column(
children: [
Wrap(
spacing: 10 * sf,
runSpacing: 10 * sf,
alignment: WrapAlignment.center,
children: possibleAssistants.map((id) {
final name = controller.playerNames[id] ?? "Desconhecido";
final number = controller.playerNumbers[id] ?? "0";
return InkWell(
onTap: () {
Navigator.pop(ctx);
controller.commitStat("add_ast", "$prefix$id");
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Assistência: $name'), duration: const Duration(seconds: 1), backgroundColor: AppTheme.successGreen));
},
child: Container(
width: 75 * sf,
padding: EdgeInsets.all(6 * sf),
decoration: BoxDecoration(
color: teamColor.withOpacity(0.2),
border: Border.all(color: teamColor, width: 1.5 * sf),
borderRadius: BorderRadius.circular(12 * sf),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
backgroundColor: teamColor,
radius: 16 * sf,
child: Text(number, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * sf)),
),
SizedBox(height: 6 * sf),
Text(name, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 10 * sf), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis),
],
),
),
);
}).toList(),
),
SizedBox(height: 15 * sf),
const Divider(color: Colors.white24),
SizedBox(height: 8 * sf),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.placarTimerBg,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 10 * sf),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8 * sf),
side: BorderSide(color: Colors.white12, width: 1 * sf),
),
),
icon: Icon(Icons.person_off, color: Colors.white, size: 16 * sf),
label: Text("Isolado (Sem assistência)", style: TextStyle(fontSize: 12 * sf)),
onPressed: () => Navigator.pop(ctx),
)
],
),
),
],
),
),
),
);
}
class TopScoreboard extends StatelessWidget {
final PlacarController controller;
final double sf;
const TopScoreboard({super.key, required this.controller, required this.sf});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 6 * sf, horizontal: 20 * sf),
decoration: BoxDecoration(
color: AppTheme.placarDarkSurface,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(22 * sf),
bottomRight: Radius.circular(22 * sf)),
border: Border.all(color: Colors.white, width: 2.0 * sf),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildTeamSection(
controller.myTeam,
controller.myScore,
controller.myFouls,
controller.myTimeoutsUsed,
AppTheme.myTeamBlue,
false,
sf),
SizedBox(width: 20 * sf),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(
horizontal: 14 * sf, vertical: 4 * sf),
decoration: BoxDecoration(
color: AppTheme.placarTimerBg,
borderRadius: BorderRadius.circular(9 * sf)),
child: ValueListenableBuilder<Duration>(
valueListenable: controller.durationNotifier,
builder: (context, duration, child) {
String formatTime =
"${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
return Text(formatTime,
style: TextStyle(
color: Colors.white,
fontSize: 24 * sf,
fontWeight: FontWeight.w900,
fontFamily: 'monospace',
letterSpacing: 1.5 * sf));
},
),
),
SizedBox(height: 4 * sf),
Text("PERÍODO ${controller.currentQuarter}",
style: TextStyle(
color: AppTheme.warningAmber,
fontSize: 12 * sf,
fontWeight: FontWeight.w900)),
],
),
SizedBox(width: 20 * sf),
_buildTeamSection(
controller.opponentTeam,
controller.opponentScore,
controller.opponentFouls,
controller.opponentTimeoutsUsed,
AppTheme.oppTeamRed,
true,
sf),
],
),
);
}
Widget _buildTeamSection(String name, int score, int fouls, int timeouts,
Color color, bool isOpp, double sf) {
int displayFouls = fouls > 5 ? 5 : fouls;
final timeoutIndicators = Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
3,
(index) => Container(
margin: EdgeInsets.symmetric(horizontal: 2.5 * sf),
width: 10 * sf,
height: 10 * sf,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: index < timeouts
? AppTheme.warningAmber
: Colors.grey.shade600,
border:
Border.all(color: Colors.white54, width: 1.0 * sf)),
)),
);
List<Widget> content = [
Column(children: [
_scoreBox(score, color, sf),
SizedBox(height: 5 * sf),
timeoutIndicators
]),
SizedBox(width: 12 * sf),
Column(
crossAxisAlignment:
isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end,
children: [
Text(name.toUpperCase(),
style: TextStyle(
color: Colors.white,
fontSize: 16 * sf,
fontWeight: FontWeight.w900,
letterSpacing: 1.0 * sf)),
SizedBox(height: 3 * sf),
Text("FALTAS: $displayFouls",
style: TextStyle(
color: displayFouls >= 5
? AppTheme.actionMiss
: AppTheme.warningAmber,
fontSize: 11 * sf,
fontWeight: FontWeight.bold)),
],
)
];
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: isOpp ? content : content.reversed.toList());
}
Widget _scoreBox(int score, Color color, double sf) => Container(
width: 45 * sf,
height: 35 * sf,
alignment: Alignment.center,
decoration: BoxDecoration(
color: color, borderRadius: BorderRadius.circular(6 * sf)),
child: Text(score.toString(),
style: TextStyle(
color: Colors.white,
fontSize: 20 * sf,
fontWeight: FontWeight.w900)),
);
}
class ShirtPainter extends CustomPainter {
final Color color;
final bool isFouledOut;
const ShirtPainter({required this.color, this.isFouledOut = false});
@override
void paint(Canvas canvas, Size size) {
final double w = size.width;
final double h = size.height;
final Color shirtColor = isFouledOut ? Colors.grey.shade700 : color;
final paint = Paint()
..color = shirtColor
..style = PaintingStyle.fill;
final trimPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = w * 0.04
..strokeJoin = StrokeJoin.round;
final path = Path();
path.moveTo(w * 0.32, h * 0.10);
path.lineTo(w * 0.18, h * 0.10);
path.quadraticBezierTo(w * 0.28, h * 0.35, w * 0.05, h * 0.55);
path.lineTo(w * 0.15, h * 1.1);
path.lineTo(w * 0.85, h * 1.1);
path.lineTo(w * 0.95, h * 0.55);
path.quadraticBezierTo(w * 0.72, h * 0.35, w * 0.82, h * 0.10);
path.lineTo(w * 0.68, h * 0.10);
path.quadraticBezierTo(w * 0.50, h * 0.45, w * 0.32, h * 0.10);
path.close();
canvas.drawPath(path, paint);
canvas.drawPath(path, trimPaint);
}
@override
bool shouldRepaint(ShirtPainter old) => old.color != color || old.isFouledOut != isFouledOut;
}
class PlayerCourtCard extends StatelessWidget {
final PlacarController controller;
final String playerId;
final bool isOpponent;
final double sf;
const PlayerCourtCard({super.key, required this.controller, required this.playerId, required this.isOpponent, required this.sf});
@override
Widget build(BuildContext context) {
final teamColor = isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue;
final realName = controller.playerNames[playerId] ?? "Erro";
final stats = controller.playerStats[playerId]!;
final number = controller.playerNumbers[playerId]!;
final prefix = isOpponent ? "player_opp_" : "player_my_";
return Draggable<String>(
data: "$prefix$playerId",
feedback: Material(
color: Colors.transparent,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(6)),
child: Text(realName, style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
),
),
childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, realName, stats, teamColor, false, false, sf)),
child: DragTarget<String>(
onAcceptWithDetails: (details) {
final action = details.data;
if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") {
bool isMake = action.startsWith("add_");
bool is3Pt = action.endsWith("_3");
showDialog(
context: context,
builder: (ctx) => ZoneMapDialog(
playerName: realName,
isMake: isMake,
is3PointAction: is3Pt,
onZoneSelected: (zone, points, relX, relY) {
Navigator.pop(ctx);
controller.registerShotFromPopup(context, action, "$prefix$playerId", zone, points, relX, relY);
if (isMake) {
showAssistDialog(context, controller, isOpponent, playerId, sf);
}
},
),
);
}
// ─── NOVO POP-UP PARA AS FALTAS ───
else if (action == "add_foul") {
showDialog(
context: context,
builder: (ctx) => ActionSubtypeDialog(
title: "TIPO DE FALTA",
options: const {
"Defensiva": "Falta Defensiva",
"Ofensiva": "Falta Ofensiva",
"Técnica": "Falta Técnica",
"Antidesportiva": "Antidesportiva",
"Desqualificante": "Desqualificante",
},
sf: sf,
onSelected: (foulType) {
Navigator.pop(ctx);
// Depois de escolher o tipo de falta, abre o pop-up a perguntar quem sofreu
showFoulVictimDialog(context, controller, isOpponent, playerId, foulType, sf);
},
),
);
}
// ─── POP-UPS PARA TOV, STL E BLK ───
else if (action == "add_tov") {
showDialog(
context: context,
builder: (ctx) => ActionSubtypeDialog(
title: "TIPO DE TURNOVER",
options: const {
"tov_3s": "3 Segundos",
"tov_clock": "Relógio Lanç.",
"tov_travel": "Passos",
"tov_double": "Dribles Duplos",
"tov_badpass": "Passe Ruim",
},
sf: sf,
onSelected: (subAction) {
Navigator.pop(ctx);
controller.handleActionDrag(context, subAction, "$prefix$playerId");
},
),
);
}
else if (action == "add_stl") {
showDialog(
context: context,
builder: (ctx) => ActionSubtypeDialog(
title: "TIPO DE ROUBO",
options: const {
"stl_steal": "Roubo de Bola",
"stl_intercept": "Interceção Lanç.",
},
sf: sf,
onSelected: (subAction) {
Navigator.pop(ctx);
controller.handleActionDrag(context, subAction, "$prefix$playerId");
},
),
);
}
else if (action == "add_blk") {
showDialog(
context: context,
builder: (ctx) => ActionSubtypeDialog(
title: "DESARME",
options: const {
"blk_made": "Fez o Desarme",
"blk_suffered": "Sofreu Desarme",
},
sf: sf,
onSelected: (subAction) {
Navigator.pop(ctx);
controller.handleActionDrag(context, subAction, "$prefix$playerId");
},
),
);
}
// ─── FIM DOS POP-UPS ESPECIAIS ───
else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) {
controller.handleActionDrag(context, action, "$prefix$playerId");
}
else if (action.startsWith("bench_")) {
controller.handleSubbing(context, action, playerId, isOpponent);
}
},
builder: (context, candidateData, rejectedData) {
bool isSubbing = candidateData.any((data) => data != null && (data.startsWith("bench_my_") || data.startsWith("bench_opp_")));
bool isActionHover = candidateData.any((data) => data != null && (data.startsWith("add_") || data.startsWith("sub_") || data.startsWith("miss_")));
return _playerCardUI(number, realName, stats, teamColor, isSubbing, isActionHover, sf);
},
),
);
}
Widget _playerCardUI(String number, String displayNameStr, Map<String, int> stats, Color teamColor, bool isSubbing, bool isActionHover, double sf) {
bool isFouledOut = stats["fls"]! >= 5;
Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white;
Color borderColor = isFouledOut ? AppTheme.actionMiss : Colors.transparent;
if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = AppTheme.myTeamBlue; }
else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = AppTheme.actionPoints; }
int fgm = stats["fgm"]!; int fga = stats["fga"]!;
String fgPercent = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) : "0";
String displayName = displayNameStr.length > 12 ? "${displayNameStr.substring(0, 10)}..." : displayNameStr;
final double shirtSize = 40 * sf;
return Container(
padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 6 * sf),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8 * sf),
border: Border.all(color: borderColor, width: 1.5 * sf),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4 * sf, offset: Offset(0, 2 * sf))],
),
child: IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: shirtSize,
height: shirtSize,
child: Stack(
alignment: Alignment.center,
children: [
CustomPaint(
size: Size(shirtSize, shirtSize),
painter: ShirtPainter(
color: teamColor,
isFouledOut: isFouledOut,
),
),
Padding(
padding: EdgeInsets.only(top: shirtSize * 0.15),
child: Text(
number,
style: TextStyle(
color: Colors.white,
fontSize: shirtSize * 0.40,
fontWeight: FontWeight.w900,
decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none,
shadows: const [Shadow(color: Colors.black45, blurRadius: 2, offset: Offset(1, 1))],
),
),
),
],
),
),
SizedBox(width: 8 * sf),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(displayName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? AppTheme.actionMiss : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)),
SizedBox(height: 1.5 * sf),
Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[700], fontWeight: FontWeight.w600)),
Text("${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls", style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[500], fontWeight: FontWeight.w600)),
],
),
],
),
),
);
}
}
class SubstitutionDialog extends StatefulWidget {
final PlacarController controller;
final bool isOpponent;
final double sf;
final String? forcedStarterId; // <--- ADICIONADO PARA EXPULSÕES
const SubstitutionDialog({
super.key,
required this.controller,
required this.isOpponent,
required this.sf,
this.forcedStarterId,
});
@override
State<SubstitutionDialog> createState() => _SubstitutionDialogState();
}
class _SubstitutionDialogState extends State<SubstitutionDialog> {
String? _selectedStarterId;
String? _selectedBenchId;
PlacarController get ctrl => widget.controller;
bool get isOpp => widget.isOpponent;
double get sf => widget.sf;
List<String> get court => isOpp ? ctrl.oppCourt : ctrl.myCourt;
List<String> get bench => isOpp ? ctrl.oppBench : ctrl.myBench;
Color get teamColor => isOpp ? AppTheme.oppTeamRed : AppTheme.myTeamBlue;
String get teamName => isOpp ? ctrl.opponentTeam : ctrl.myTeam;
bool get canConfirm => _selectedStarterId != null && _selectedBenchId != null;
bool get isForced => widget.forcedStarterId != null; // NOVO
@override
void initState() {
super.initState();
// Se for obrigado a sair, já aparece selecionado!
if (isForced) {
_selectedStarterId = widget.forcedStarterId;
}
}
void _confirmSwap() {
if (!canConfirm) return;
final benchPrefix = isOpp ? "bench_opp_" : "bench_my_";
ctrl.handleSubbing(
context,
"$benchPrefix$_selectedBenchId",
_selectedStarterId!,
isOpp,
);
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
const activeColor = Color(0xFF22C55E);
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.all(16 * sf),
child: Container(
width: 420 * sf,
decoration: BoxDecoration(
color: const Color(0xFF1A1F2E),
borderRadius: BorderRadius.circular(14 * sf),
border: Border.all(color: isForced ? AppTheme.actionMiss : const Color(0xFF2D3450), width: 2), // Borda vermelha se for expulsão
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 10 * sf),
decoration: BoxDecoration(
color: isForced ? AppTheme.actionMiss.withOpacity(0.8) : const Color(0xFF1E2540), // Fundo vermelho no título
borderRadius: BorderRadius.vertical(top: Radius.circular(12 * sf)),
border: const Border(bottom: BorderSide(color: Color(0xFF2D3450))),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isForced ? "SUBSTITUIÇÃO OBRIGATÓRIA (5 Faltas)" : "Substituição — ${teamName.toUpperCase()}",
style: TextStyle(color: Colors.white, fontSize: 13 * sf, fontWeight: FontWeight.w600),
),
if (!isForced) // Esconde o "X" de fechar se for forçado
InkWell(
onTap: () => Navigator.pop(context),
child: Container(
padding: EdgeInsets.all(4 * sf),
decoration: const BoxDecoration(color: Colors.white24, shape: BoxShape.circle),
child: Icon(Icons.close, color: Colors.white, size: 14 * sf),
),
),
],
),
),
_sectionLabel("Em Campo"),
_playerGrid(
players: court.where((id) => !id.startsWith("fake_")).toList(),
selected: _selectedStarterId,
isStarter: true,
activeColor: activeColor,
onTap: (id) {
if (isForced) return; // Se for forçado, não deixa clicar/desmarcar o titular
setState(() {
_selectedStarterId = _selectedStarterId == id ? null : id;
});
},
),
Divider(color: Colors.white12, height: 1, indent: 10 * sf, endIndent: 10 * sf),
_sectionLabel("Banco de Suplentes"),
_playerGrid(
players: bench.where((id) => !id.startsWith("fake_")).toList(),
selected: _selectedBenchId,
isStarter: false,
activeColor: activeColor,
onTap: (id) {
final fouls = ctrl.playerStats[id]?["fls"] ?? 0;
if (fouls >= 5) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('🛑 ${ctrl.playerNames[id]} expulso!'),
backgroundColor: AppTheme.actionMiss));
return;
}
setState(() {
_selectedBenchId = _selectedBenchId == id ? null : id;
});
},
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf),
child: Text(
_hintText(),
style: TextStyle(color: isForced ? AppTheme.actionMiss : Colors.white38, fontSize: 11 * sf, fontWeight: isForced ? FontWeight.bold : FontWeight.normal),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.fromLTRB(12 * sf, 0, 12 * sf, 12 * sf),
child: Row(
children: [
if (!isForced) // Esconde o botão de cancelar
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white12,
foregroundColor: Colors.white70,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8 * sf)),
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 10 * sf),
elevation: 0,
),
onPressed: () => Navigator.pop(context),
child: Text("Cancelar", style: TextStyle(fontSize: 13 * sf)),
),
if (!isForced) SizedBox(width: 8 * sf),
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: canConfirm ? activeColor : Colors.white12,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8 * sf)),
padding: EdgeInsets.symmetric(vertical: 10 * sf),
elevation: 0,
),
onPressed: canConfirm ? _confirmSwap : null,
child: Text(isForced ? "Substituir Jogador" : "Confirmar Troca", style: TextStyle(fontSize: 13 * sf, fontWeight: FontWeight.w600)),
),
),
],
),
),
],
),
),
);
}
String _hintText() {
if (isForced && _selectedBenchId == null) {
return "Um jogador atingiu as 5 faltas. Seleciona um suplente obrigatoriamente.";
} else if (_selectedStarterId == null && _selectedBenchId == null) {
return "Seleciona um titular e um suplente para fazer a troca";
} else if (_selectedStarterId != null && _selectedBenchId == null) {
return "Agora seleciona o suplente que vai entrar";
} else if (_selectedStarterId == null && _selectedBenchId != null) {
return "Agora seleciona o titular que vai sair";
} else {
final s = ctrl.playerNames[_selectedStarterId] ?? "";
final sNum = ctrl.playerNumbers[_selectedStarterId] ?? "";
final b = ctrl.playerNames[_selectedBenchId] ?? "";
final bNum = ctrl.playerNumbers[_selectedBenchId] ?? "";
return "#$sNum $s ↔ #$bNum $b";
}
}
Widget _sectionLabel(String label) => Padding(
padding: EdgeInsets.fromLTRB(12 * sf, 8 * sf, 12 * sf, 4 * sf),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
label.toUpperCase(),
style: TextStyle(color: Colors.white38, fontSize: 10 * sf, letterSpacing: 0.8, fontWeight: FontWeight.w500),
),
),
);
Widget _playerGrid({
required List<String> players,
required String? selected,
required bool isStarter,
required Color activeColor,
required void Function(String) onTap,
}) {
return Padding(
padding: EdgeInsets.fromLTRB(10 * sf, 0, 10 * sf, 6 * sf),
child: Wrap(
spacing: 6 * sf,
runSpacing: 6 * sf,
children: players.map((id) {
final isSelected = selected == id;
final name = ctrl.playerNames[id] ?? "?";
final num = ctrl.playerNumbers[id] ?? "0";
final fouls = ctrl.playerStats[id]?["fls"] ?? 0;
final isFouledOut = fouls >= 5;
final shortName = name.length > 8 ? "${name.substring(0, 7)}." : name;
final bgColor = isSelected ? const Color(0xFF14331F) : isStarter ? const Color(0xFF1E2540) : const Color(0xFF141824);
final borderColor = isSelected ? activeColor : Colors.transparent;
final shirtColor = isSelected ? const Color(0xFF15803D) : isFouledOut ? Colors.grey.shade700 : teamColor;
return GestureDetector(
onTap: () => onTap(id),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 60 * sf,
padding: EdgeInsets.symmetric(vertical: 6 * sf, horizontal: 2 * sf),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(10 * sf),
border: Border.all(color: borderColor, width: 2),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 36 * sf,
height: 36 * sf,
child: Stack(
alignment: Alignment.center,
children: [
CustomPaint(
size: Size(36 * sf, 36 * sf),
painter: ShirtPainter(color: shirtColor, isFouledOut: isFouledOut),
),
Padding(
padding: EdgeInsets.only(top: 36 * sf * 0.15),
child: Text(
num,
style: TextStyle(
color: Colors.white,
fontSize: 36 * sf * 0.38,
fontWeight: FontWeight.w900,
decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none,
shadows: const [Shadow(color: Colors.black45, blurRadius: 2)],
),
),
),
],
),
),
SizedBox(height: 3 * sf),
Text(
shortName,
style: TextStyle(color: Colors.white70, fontSize: 9 * sf, fontWeight: FontWeight.w500),
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
if (isFouledOut)
Container(
margin: EdgeInsets.only(top: 2 * sf),
padding: EdgeInsets.symmetric(horizontal: 4 * sf, vertical: 1 * sf),
decoration: BoxDecoration(
color: Colors.red.shade900.withOpacity(0.4),
border: Border.all(color: Colors.red.shade400),
borderRadius: BorderRadius.circular(4 * sf),
),
child: Text("5 Fls", style: TextStyle(color: Colors.red.shade300, fontSize: 8 * sf)),
),
],
),
),
);
}).toList(),
),
);
}
}
class HeatmapDialog extends StatefulWidget {
final List<dynamic> shots;
final String myTeamName;
final String oppTeamName;
final List<String> myPlayersIds;
final List<String> oppPlayersIds;
final Map<String, Map<String, int>> playerStats;
final Map<String, String> playerNames;
const HeatmapDialog({
super.key,
required this.shots,
required this.myTeamName,
required this.oppTeamName,
required this.myPlayersIds,
required this.oppPlayersIds,
required this.playerStats,
required this.playerNames,
});
@override
State<HeatmapDialog> createState() => _HeatmapDialogState();
}
class _HeatmapDialogState extends State<HeatmapDialog> {
bool _isMapVisible = false;
String _selectedTeam = '';
String _selectedPlayerId = '';
@override
Widget build(BuildContext context) {
const Color headerColor = Color(0xFFE88F15);
const Color yellowBackground = Color(0xFFDFAB00);
final double screenHeight = MediaQuery.of(context).size.height;
final double dialogHeight = screenHeight * 0.95;
final double dialogWidth = dialogHeight * 1.0;
return Dialog(
backgroundColor: yellowBackground,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.antiAlias,
insetPadding: const EdgeInsets.all(10),
child: SizedBox(
height: dialogHeight,
width: dialogWidth,
child: _isMapVisible ? _buildMapScreen(headerColor) : _buildSelectionScreen(headerColor),
),
);
}
Widget _buildSelectionScreen(Color headerColor) {
return Column(
children: [
Container(
height: 40,
color: headerColor,
width: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
const Text("ESCOLHE A EQUIPA OU UM JOGADOR", style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold)),
Positioned(
right: 8,
child: InkWell(
onTap: () => Navigator.pop(context),
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
child: Icon(Icons.close, color: headerColor, size: 16),
),
),
)
],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(child: _buildTeamColumn(teamName: widget.myTeamName, playerIds: widget.myPlayersIds, teamColor: AppTheme.myTeamBlue)),
const SizedBox(width: 8),
Expanded(child: _buildTeamColumn(teamName: widget.oppTeamName, playerIds: widget.oppPlayersIds, teamColor: AppTheme.oppTeamRed)),
],
),
),
),
],
);
}
Widget _buildTeamColumn({required String teamName, required List<String> playerIds, required Color teamColor}) {
final realPlayerIds = playerIds.where((id) => !id.startsWith("fake_")).toList();
return Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
child: Column(
children: [
InkWell(
onTap: () => setState(() {
_selectedTeam = teamName;
_selectedPlayerId = 'Todos';
_isMapVisible = true;
}),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: teamColor,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)),
),
child: Column(
children: [
Text(teamName.toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(12)),
child: const Text("MAPA GERAL DA EQUIPA", style: TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)),
),
],
),
),
),
Expanded(
child: ListView.separated(
itemCount: realPlayerIds.length,
separatorBuilder: (context, index) => const Divider(height: 1, color: Colors.black12),
itemBuilder: (context, index) {
final pId = realPlayerIds[index];
final pName = widget.playerNames[pId] ?? 'Desconhecido';
final pts = widget.playerStats[pId]?['pts'] ?? 0;
return ListTile(
dense: true,
visualDensity: VisualDensity.compact,
leading: Icon(Icons.person, color: teamColor),
title: Text(pName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)),
trailing: Text("$pts Pts", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: teamColor)),
onTap: () => setState(() {
_selectedTeam = teamName;
_selectedPlayerId = pId;
_isMapVisible = true;
}),
);
},
),
),
],
),
);
}
Widget _buildMapScreen(Color headerColor) {
final filteredShots = widget.shots.where((s) {
if (_selectedPlayerId != 'Todos') return s.playerId == _selectedPlayerId;
if (_selectedTeam == widget.myTeamName) return widget.myPlayersIds.contains(s.playerId);
if (_selectedTeam == widget.oppTeamName) return widget.oppPlayersIds.contains(s.playerId);
return true;
}).toList();
final titleText = _selectedPlayerId == 'Todos'
? "MAPA GERAL: ${_selectedTeam.toUpperCase()}"
: "MAPA: ${widget.playerNames[_selectedPlayerId]?.toUpperCase() ?? ''}";
return Column(
children: [
Container(
height: 40,
color: headerColor,
width: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
left: 8,
child: InkWell(
onTap: () => setState(() => _isMapVisible = false),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
child: Row(children: [
Icon(Icons.arrow_back, color: headerColor, size: 14),
const SizedBox(width: 4),
Text("VOLTAR", style: TextStyle(color: headerColor, fontWeight: FontWeight.bold, fontSize: 12)),
]),
),
),
),
Text(titleText, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold)),
Positioned(
right: 8,
child: InkWell(
onTap: () => Navigator.pop(context),
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
child: Icon(Icons.close, color: headerColor, size: 16),
),
),
),
],
),
),
Expanded(
child: LayoutBuilder(builder: (context, constraints) {
return Stack(
children: [
CustomPaint(
size: Size(constraints.maxWidth, constraints.maxHeight),
painter: HeatmapCourtPainter(),
),
...filteredShots.map((shot) => Positioned(
left: (shot.relativeX * constraints.maxWidth) - 8,
top: (shot.relativeY * constraints.maxHeight) - 8,
child: CircleAvatar(
radius: 8,
backgroundColor: shot.isMake ? AppTheme.successGreen : AppTheme.actionMiss,
child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white),
),
)),
],
);
}),
),
],
);
}
}
class HeatmapCourtPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final double w = size.width;
final double h = size.height;
final double basketX = w / 2;
final Paint whiteStroke = Paint()..color = Colors.white..style = PaintingStyle.stroke..strokeWidth = 2.0;
final Paint blackStroke = Paint()..color = Colors.black87..style = PaintingStyle.stroke..strokeWidth = 2.0;
final double margin = w * 0.10;
final double length = h * 0.35;
final double larguraDoArco = (w / 2) - margin;
final double alturaDoArco = larguraDoArco * 0.30;
final double totalArcoHeight = alturaDoArco * 4;
canvas.drawLine(Offset(margin, 0), Offset(margin, length), whiteStroke);
canvas.drawLine(Offset(w - margin, 0), Offset(w - margin, length), whiteStroke);
canvas.drawLine(Offset(0, length), Offset(margin, length), whiteStroke);
canvas.drawLine(Offset(w - margin, length), Offset(w, length), whiteStroke);
canvas.drawArc(Rect.fromCenter(center: Offset(basketX, length), width: larguraDoArco * 2, height: totalArcoHeight), 0, math.pi, false, whiteStroke);
double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75));
double sYL = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.75));
double sXR = basketX + (larguraDoArco * math.cos(math.pi * 0.25));
double sYR = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.25));
canvas.drawLine(Offset(sXL, sYL), Offset(0, h * 0.85), whiteStroke);
canvas.drawLine(Offset(sXR, sYR), Offset(w, h * 0.85), whiteStroke);
final double pW = w * 0.28;
final double pH = h * 0.38;
canvas.drawRect(Rect.fromLTWH(basketX - pW / 2, 0, pW, pH), blackStroke);
final double ftR = pW / 2;
canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), 0, math.pi, false, blackStroke);
for (int i = 0; i < 10; i++) {
canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), math.pi + (i * 2 * (math.pi / 20)), math.pi / 20, false, blackStroke);
}
canvas.drawLine(Offset(basketX - pW / 2, pH), Offset(sXL, sYL), blackStroke);
canvas.drawLine(Offset(basketX + pW / 2, pH), Offset(sXR, sYR), blackStroke);
canvas.drawArc(Rect.fromCircle(center: Offset(basketX, h), radius: w * 0.12), math.pi, math.pi, false, blackStroke);
canvas.drawCircle(Offset(basketX, h * 0.12), w * 0.02, blackStroke);
canvas.drawLine(Offset(basketX - w * 0.08, h * 0.12 - 5), Offset(basketX + w * 0.08, h * 0.12 - 5), blackStroke);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class PlayByPlayDialog extends StatelessWidget {
final PlacarController controller;
const PlayByPlayDialog({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: AppTheme.placarDarkSurface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
width: 400,
height: MediaQuery.of(context).size.height * 0.8,
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("HISTÓRICO DE JOGADAS", style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
IconButton(icon: const Icon(Icons.close, color: Colors.white), onPressed: () => Navigator.pop(context))
],
),
const Divider(color: Colors.white24),
Expanded(
child: controller.playByPlay.isEmpty
? const Center(child: Text("Ainda não há jogadas.", style: TextStyle(color: Colors.white54)))
: ListView.separated(
itemCount: controller.playByPlay.length,
separatorBuilder: (_, __) => const Divider(color: Colors.white10),
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(controller.playByPlay[index], style: const TextStyle(color: Colors.white, fontSize: 14)),
),
),
),
],
),
),
);
}
}
class BoxScoreDialog extends StatelessWidget {
final PlacarController controller;
final double sf;
const BoxScoreDialog({super.key, required this.controller, required this.sf});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Dialog(
backgroundColor: AppTheme.placarDarkSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12 * sf),
side: BorderSide(color: Colors.white24, width: 1 * sf),
),
insetPadding: EdgeInsets.all(8 * sf),
clipBehavior: Clip.antiAlias,
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.98,
height: MediaQuery.of(context).size.height * 0.98,
child: DefaultTabController(
length: 2,
child: Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(16 * sf, 16 * sf, 8 * sf, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("BOX SCORE", style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)),
IconButton(
icon: Icon(Icons.close, color: Colors.white, size: 24 * sf),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () => Navigator.pop(context),
)
],
),
),
SizedBox(height: 8 * sf),
TabBar(
indicatorColor: AppTheme.warningAmber,
labelColor: Colors.white,
unselectedLabelColor: Colors.white54,
labelStyle: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold),
indicatorWeight: 3 * sf,
dividerColor: Colors.white10,
tabs: [
Tab(text: controller.myTeam.toUpperCase(), height: 40 * sf),
Tab(text: controller.opponentTeam.toUpperCase(), height: 40 * sf),
],
),
Expanded(
child: Container(
width: double.infinity,
color: Colors.black12,
child: ValueListenableBuilder<Duration>(
valueListenable: controller.durationNotifier,
builder: (context, duration, _) {
return TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: [
_buildStatsTable(controller.myCourt + controller.myBench, controller, sf),
_buildStatsTable(controller.oppCourt + controller.oppBench, controller, sf),
],
);
},
),
),
),
],
),
),
),
);
},
);
}
Widget _buildStatsTable(List<String> teamPlayers, PlacarController ctrl, double sf) {
return LayoutBuilder(builder: (context, constraints) {
return SingleChildScrollView(
scrollDirection: Axis.vertical,
physics: const BouncingScrollPhysics(),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const ClampingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: constraints.maxWidth),
child: DataTable(
headingRowColor: WidgetStateProperty.all(AppTheme.placarListCard),
columnSpacing: 18 * sf,
horizontalMargin: 16 * sf,
headingRowHeight: 45 * sf,
dataRowMinHeight: 40 * sf,
dataRowMaxHeight: 45 * sf,
headingTextStyle: TextStyle(color: Colors.white70, fontWeight: FontWeight.bold, fontSize: 13 * sf),
dataTextStyle: TextStyle(color: Colors.white, fontSize: 13 * sf),
columns: const [
DataColumn(label: Text('JOGADOR')),
DataColumn(label: Text('MIN')),
DataColumn(label: Text('PTS')),
DataColumn(label: Text('REB')),
DataColumn(label: Text('AST')),
DataColumn(label: Text('STL')),
DataColumn(label: Text('BLK')),
DataColumn(label: Text('TOV')),
DataColumn(label: Text('FLS')),
DataColumn(label: Text('SO')),
DataColumn(label: Text('IL')),
DataColumn(label: Text('LI')),
DataColumn(label: Text('PA')),
DataColumn(label: Text('3S')),
DataColumn(label: Text('DR')),
DataColumn(label: Text('FG')),
],
rows: teamPlayers.where((id) => !id.startsWith("fake_")).map((id) {
final name = ctrl.playerNames[id] ?? "---";
final s = ctrl.playerStats[id]!;
final totalSecs = s['sec'] ?? 0;
final minutes = totalSecs ~/ 60;
final seconds = totalSecs % 60;
final timeStr = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
final rebs = s['orb']! + s['drb']!;
final fgText = "${s['fgm']}/${s['fga']}";
return DataRow(cells: [
DataCell(Text(name, style: const TextStyle(fontWeight: FontWeight.bold))),
DataCell(Text(timeStr, style: const TextStyle(color: Colors.white70))),
DataCell(Text(s['pts'].toString(), style: TextStyle(color: AppTheme.warningAmber, fontWeight: FontWeight.bold, fontSize: 14 * sf))),
DataCell(Text(rebs.toString())),
DataCell(Text(s['ast'].toString())),
DataCell(Text(s['stl'].toString())),
DataCell(Text(s['blk'].toString())),
DataCell(Text(s['tov'].toString(), style: const TextStyle(color: Colors.redAccent))),
DataCell(Text(s['fls'].toString())),
DataCell(Text((s['so'] ?? 0).toString(), style: const TextStyle(color: Colors.greenAccent))),
DataCell(Text((s['il'] ?? 0).toString(), style: const TextStyle(color: Colors.lightBlue))),
DataCell(Text((s['li'] ?? 0).toString(), style: const TextStyle(color: Colors.orangeAccent))),
DataCell(Text((s['pa'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))),
DataCell(Text((s['tres_seg'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), // CORRIGIDO PARA MOSTRAR OS 3 SEG NO BOX SCORE
DataCell(Text((s['dr'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))),
DataCell(Text(fgText, style: const TextStyle(color: Colors.white54))),
]);
}).toList(),
),
),
),
);
});
}
}