pdf e exel

This commit is contained in:
2026-05-06 12:47:17 +01:00
parent c3a90f2816
commit 60656d77e8
14 changed files with 1512 additions and 951 deletions

View File

@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class ActiveTeam {
final String id;
final String name;
final String? logo;
final int wins;
final int losses;
final int draws;
ActiveTeam({
required this.id,
required this.name,
this.logo,
this.wins = 0,
this.losses = 0,
this.draws = 0,
});
}
// 🟢 A MÁGICA: Esta variável avisa a Home e a StatusPage ao mesmo tempo quando a equipa muda!
final ValueNotifier<ActiveTeam?> globalActiveTeam = ValueNotifier(null);
// 🟢 FUNÇÃO PARA CARREGAR A EQUIPA AO ABRIR A APP (Lê da Memória e do Supabase)
Future<void> loadGlobalTeam() async {
final prefs = await SharedPreferences.getInstance();
final savedId = prefs.getString('last_team_id');
// 1. Carrega rápido da memória (para não piscar o ecrã)
if (savedId != null) {
globalActiveTeam.value = ActiveTeam(
id: savedId,
name: prefs.getString('last_team_name') ?? "Selecionar Equipa",
logo: prefs.getString('last_team_logo'),
wins: prefs.getInt('last_team_wins') ?? 0,
losses: prefs.getInt('last_team_losses') ?? 0,
draws: prefs.getInt('last_team_draws') ?? 0,
);
}
// 2. Vai confirmar no Supabase se entraste com esta conta noutro telemóvel!
final supabase = Supabase.instance.client;
final userId = supabase.auth.currentUser?.id;
if (userId == null) return;
try {
final profile = await supabase.from('profiles').select('selected_team_id').eq('id', userId).maybeSingle();
if (profile != null && profile['selected_team_id'] != null) {
final dbTeamId = profile['selected_team_id'].toString();
final teamData = await supabase.from('teams').select().eq('id', dbTeamId).maybeSingle();
if (teamData != null) {
final newTeam = ActiveTeam(
id: teamData['id'].toString(),
name: teamData['name'] ?? 'Desconhecido',
logo: teamData['image_url'],
wins: int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0,
losses: int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0,
draws: int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0,
);
globalActiveTeam.value = newTeam;
// Atualiza a memória do telemóvel para a próxima vez ser rápido
await prefs.setString('last_team_id', newTeam.id);
await prefs.setString('last_team_name', newTeam.name);
if (newTeam.logo != null && newTeam.logo!.isNotEmpty) {
await prefs.setString('last_team_logo', newTeam.logo!);
}
await prefs.setInt('last_team_wins', newTeam.wins);
await prefs.setInt('last_team_losses', newTeam.losses);
await prefs.setInt('last_team_draws', newTeam.draws);
}
}
} catch (e) {
debugPrint("Erro ao carregar equipa do Supabase: $e");
}
}
// 🟢 FUNÇÃO PARA GUARDAR A EQUIPA (Na Memória e no Supabase)
Future<void> saveGlobalTeam(ActiveTeam team) async {
globalActiveTeam.value = team; // Atualiza a app inteira!
// 1. Guarda no telemóvel
final prefs = await SharedPreferences.getInstance();
await prefs.setString('last_team_id', team.id);
await prefs.setString('last_team_name', team.name);
if (team.logo != null && team.logo!.isNotEmpty) {
await prefs.setString('last_team_logo', team.logo!);
} else {
await prefs.remove('last_team_logo');
}
await prefs.setInt('last_team_wins', team.wins);
await prefs.setInt('last_team_losses', team.losses);
await prefs.setInt('last_team_draws', team.draws);
// 2. Guarda no Supabase!
final supabase = Supabase.instance.client;
final userId = supabase.auth.currentUser?.id;
if (userId != null) {
try {
await supabase.from('profiles').upsert({
'id': userId,
'selected_team_id': team.id,
});
} catch (e) {
debugPrint("Erro ao guardar equipa no Supabase: $e");
}
}
}

View File

@@ -0,0 +1,375 @@
import 'dart:io';
import 'package:excel/excel.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:flutter/material.dart' hide Border, BorderStyle;
class ExcelExportService {
static Future<void> generateAndPrintBoxScoreExcel({
required String gameId,
required String myTeam,
required String opponentTeam,
required String myScore,
required String opponentScore,
required String season,
required String targetTeam,
}) async {
try {
final supabase = Supabase.instance.client;
// ── 1. DADOS DO JOGO ───────────────────────────────────────────────────
final gameData = await supabase.from('games').select().eq('id', gameId).maybeSingle();
String dateStr = "---";
if (gameData != null && gameData['game_date'] != null) {
String rawDate = gameData['game_date'].toString();
dateStr = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
}
// ── 2. ESTATÍSTICAS DOS JOGADORES ──────────────────────────────────────
final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId);
if (statsResponse.isEmpty) return;
// ── 3. NOMES E NÚMEROS DAS EQUIPAS E JOGADORES ───────────────────────
final membersResponse = await supabase.from('members').select('id, name, number');
final Map<String, Map<String, dynamic>> memberInfo = {
for (var m in membersResponse) m['id'].toString(): m
};
final teamsResponse = await supabase.from('teams').select('id, name');
final Map<String, String> teamNames = {
for (var t in teamsResponse) t['id'].toString(): t['name'].toString()
};
// ── 4. CONFIGURAÇÃO DO EXCEL ───────────────────────────────────────────
var excel = Excel.createExcel();
String sheetName = 'Estatísticas';
Sheet sheet = excel[sheetName];
excel.setDefaultSheet(sheetName);
if (excel.tables.keys.contains('Sheet1')) excel.delete('Sheet1');
// ── ESTILOS E CORES PREMIUM ───────────────────────────────────────────
final corPrincipal = ExcelColor.fromHexString('#A00000'); // Vermelho escuro
final corFundoCinza = ExcelColor.fromHexString('#F5F5F5');
final corFundoCinzaEscuro = ExcelColor.fromHexString('#E0E0E0');
final cor2P = ExcelColor.fromHexString('#E3F2FD'); // Azul claro
final cor3P = ExcelColor.fromHexString('#E8F5E9'); // Verde claro
final corGlobal = ExcelColor.fromHexString('#FFF9C4');// Amarelo claro
final corLL = ExcelColor.fromHexString('#FFF3E0'); // Laranja claro
final corReb = ExcelColor.fromHexString('#F3E5F5'); // Roxo claro
final borderGrey = ExcelColor.fromHexString('#BDBDBD');
CellStyle styleTituloJogo = CellStyle(bold: true, fontSize: 16);
CellStyle styleNomeEquipa = CellStyle(bold: true, fontSize: 14, fontColorHex: ExcelColor.white, backgroundColorHex: corPrincipal, horizontalAlign: HorizontalAlign.Center, verticalAlign: VerticalAlign.Center);
CellStyle styleTituloSecundario = CellStyle(bold: true, fontSize: 12, fontColorHex: ExcelColor.black, backgroundColorHex: corFundoCinzaEscuro, horizontalAlign: HorizontalAlign.Center, verticalAlign: VerticalAlign.Center);
CellStyle styleGrelha(ExcelColor bgCol, {bool isBold = false}) {
return CellStyle(
bold: isBold, backgroundColorHex: bgCol,
horizontalAlign: HorizontalAlign.Center, verticalAlign: VerticalAlign.Center,
leftBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
rightBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
topBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
bottomBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
);
}
final styleGeral = styleGrelha(ExcelColor.white);
final styleGeralBold = styleGrelha(ExcelColor.white, isBold: true);
final styleNome = CellStyle(horizontalAlign: HorizontalAlign.Left, verticalAlign: VerticalAlign.Center, leftBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey), rightBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey), topBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey), bottomBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey));
// ── CABEÇALHO DO JOGO ────────────────────────────────────────────────
sheet.cell(CellIndex.indexByString("A1")).value = TextCellValue("JOGO:");
sheet.cell(CellIndex.indexByString("A1")).cellStyle = CellStyle(bold: true);
sheet.cell(CellIndex.indexByString("B1")).value = TextCellValue("$myTeam vs $opponentTeam");
sheet.cell(CellIndex.indexByString("B1")).cellStyle = styleTituloJogo;
sheet.cell(CellIndex.indexByString("A2")).value = TextCellValue("COMPETIÇÃO:");
sheet.cell(CellIndex.indexByString("A2")).cellStyle = CellStyle(bold: true);
sheet.cell(CellIndex.indexByString("B2")).value = TextCellValue(season);
sheet.cell(CellIndex.indexByString("A3")).value = TextCellValue("DATA:");
sheet.cell(CellIndex.indexByString("A3")).cellStyle = CellStyle(bold: true);
sheet.cell(CellIndex.indexByString("B3")).value = TextCellValue(dateStr);
sheet.cell(CellIndex.indexByString("A4")).value = TextCellValue("RESULTADO:");
sheet.cell(CellIndex.indexByString("A4")).cellStyle = CellStyle(bold: true);
sheet.cell(CellIndex.indexByString("B4")).value = TextCellValue("$myScore - $opponentScore");
sheet.cell(CellIndex.indexByString("B4")).cellStyle = CellStyle(bold: true, fontColorHex: corPrincipal);
// ── TOTAIS POR PERÍODO (NOVA SECÇÃO) ─────────────────────────────────
sheet.cell(CellIndex.indexByString("A6")).value = TextCellValue("PONTUAÇÃO POR PERÍODO");
sheet.cell(CellIndex.indexByString("A6")).cellStyle = CellStyle(bold: true, fontColorHex: corPrincipal);
List<String> periodHeaders = ["EQUIPA", "1º Q", "2º Q", "3º Q", "4º Q", "TOTAL"];
for (int i = 0; i < periodHeaders.length; i++) {
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 6));
cell.value = TextCellValue(periodHeaders[i]);
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
}
// Linha Minha Equipa
List<dynamic> myRow = [
myTeam,
gameData?['my_q1']?.toString() ?? '-',
gameData?['my_q2']?.toString() ?? '-',
gameData?['my_q3']?.toString() ?? '-',
gameData?['my_q4']?.toString() ?? '-',
myScore
];
for (int i = 0; i < myRow.length; i++) {
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 7));
cell.value = TextCellValue(myRow[i].toString());
cell.cellStyle = i == 0 ? styleNome : styleGeralBold;
}
// Linha Adversário
List<dynamic> oppRow = [
opponentTeam,
gameData?['opp_q1']?.toString() ?? '-',
gameData?['opp_q2']?.toString() ?? '-',
gameData?['opp_q3']?.toString() ?? '-',
gameData?['opp_q4']?.toString() ?? '-',
opponentScore
];
for (int i = 0; i < oppRow.length; i++) {
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 8));
cell.value = TextCellValue(oppRow[i].toString());
cell.cellStyle = i == 0 ? styleNome : styleGeralBold;
}
int r = 11; // 👈 AS TABELAS PRINCIPAIS AGORA COMEÇAM MAIS ABAIXO (Linha 12 no Excel)
// Agrupar estatísticas por equipa
Map<String, List<dynamic>> statsByTeam = {};
for(var s in statsResponse) {
String tId = s['team_id'].toString();
statsByTeam.putIfAbsent(tId, () => []).add(s);
}
// ── CONSTRUÇÃO DAS TABELAS DE CADA EQUIPA ────────────────────────────
for (var entry in statsByTeam.entries) {
String tId = entry.key;
List<dynamic> tStats = entry.value;
String tName = teamNames[tId] ?? "Equipa $tId";
if (targetTeam != 'Ambas' && tName != targetTeam) continue;
tStats.sort((a, b) {
var mInfoA = memberInfo[a['member_id'].toString()];
var mInfoB = memberInfo[b['member_id'].toString()];
int numA = int.tryParse(mInfoA?['number']?.toString() ?? '0') ?? 0;
int numB = int.tryParse(mInfoB?['number']?.toString() ?? '0') ?? 0;
return numA.compareTo(numB);
});
List<Map<String, dynamic>> processedPlayers = [];
int tMin=0, tPts=0, t2m=0, t2a=0, t3m=0, t3a=0, tFgm=0, tFga=0, tftm=0, tfta=0;
int torb=0, tdrb=0, tStl=0, tAst=0, tTov=0, tBlk=0, tFls=0;
int tSo=0, tIl=0, tLi=0, tPa=0, tTresS=0, tDr=0;
for(var stat in tStats) {
var mInfo = memberInfo[stat['member_id'].toString()];
String pNum = mInfo != null ? (mInfo['number']?.toString() ?? "-") : "-";
String pName = mInfo != null ? (mInfo['name']?.toString() ?? "Desconhecido") : "Desconhecido";
int minSecs = stat['minutos_jogados'] ?? 0;
int pts = stat['pts'] ?? 0;
int p2m = stat['p2m'] ?? 0; int p2a = stat['p2a'] ?? 0;
int p3m = stat['p3m'] ?? 0; int p3a = stat['p3a'] ?? 0;
int fgm = stat['fgm'] ?? 0; int fga = stat['fga'] ?? 0;
int ftm = stat['ftm'] ?? 0; int fta = stat['fta'] ?? 0;
int orb = stat['orb'] ?? 0; int drb = stat['drb'] ?? 0; int tr = orb + drb;
int stl = stat['stl'] ?? 0; int ast = stat['ast'] ?? 0;
int tov = stat['tov'] ?? 0; int blk = stat['blk'] ?? 0; int fls = stat['fls'] ?? 0;
int so = stat['so'] ?? 0; int il = stat['il'] ?? 0; int li = stat['li'] ?? 0;
int pa = stat['pa'] ?? 0; int tresS = stat['tres_seg'] ?? 0; int dr = stat['dr'] ?? 0;
tMin+=minSecs; tPts+=pts; t2m+=p2m; t2a+=p2a; t3m+=p3m; t3a+=p3a;
tFgm+=fgm; tFga+=fga; tftm+=ftm; tfta+=fta; torb+=orb; tdrb+=drb;
tStl+=stl; tAst+=ast; tTov+=tov; tBlk+=blk; tFls+=fls;
tSo+=so; tIl+=il; tLi+=li; tPa+=pa; tTresS+=tresS; tDr+=dr;
processedPlayers.add({
'num': pNum, 'name': pName, 'minSecs': minSecs, 'pts': pts,
'p2m': p2m, 'p2a': p2a, 'p3m': p3m, 'p3a': p3a, 'fgm': fgm, 'fga': fga,
'ftm': ftm, 'fta': fta, 'orb': orb, 'drb': drb, 'tr': tr,
'stl': stl, 'ast': ast, 'tov': tov, 'blk': blk, 'fls': fls,
'so': so, 'il': il, 'li': li, 'pa': pa, '3s': tresS, 'dr': dr
});
}
// TABELA 1: LANÇAMENTOS E RESSALTOS
var teamStart = CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r);
var teamEnd = CellIndex.indexByColumnRow(columnIndex: 18, rowIndex: r);
sheet.merge(teamStart, teamEnd, customValue: TextCellValue("ESTATÍSTICAS DA EQUIPA: ${tName.toUpperCase()} (Lançamentos e Ressaltos)"));
for(int i=0; i<=18; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleNomeEquipa;
r++;
void criarCategoria(int colStart, int colEnd, String texto, CellStyle estilo) {
sheet.merge(CellIndex.indexByColumnRow(columnIndex: colStart, rowIndex: r), CellIndex.indexByColumnRow(columnIndex: colEnd, rowIndex: r), customValue: TextCellValue(texto));
for(int i=colStart; i<=colEnd; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = estilo;
}
for(int i=0; i<=3; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleGrelha(corFundoCinza);
criarCategoria(4, 6, "2 PONTOS", styleGrelha(cor2P, isBold: true));
criarCategoria(7, 9, "3 PONTOS", styleGrelha(cor3P, isBold: true));
criarCategoria(10, 12, "GLOBAL", styleGrelha(corGlobal, isBold: true));
criarCategoria(13, 15, "L. LIVRES", styleGrelha(corLL, isBold: true));
criarCategoria(16, 18, "RESSALTOS", styleGrelha(corReb, isBold: true));
r++;
List<String> colsT1 = ["", "NOME", "MIN", "PTS", "C", "T", "%", "C", "T", "%", "C", "T", "%", "C", "T", "%", "RO", "RD", "TR"];
for(int i = 0; i < colsT1.length; i++) {
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
cell.value = TextCellValue(colsT1[i]);
if (i >= 4 && i <= 6) cell.cellStyle = styleGrelha(cor2P, isBold: true);
else if (i >= 7 && i <= 9) cell.cellStyle = styleGrelha(cor3P, isBold: true);
else if (i >= 10 && i <= 12) cell.cellStyle = styleGrelha(corGlobal, isBold: true);
else if (i >= 13 && i <= 15) cell.cellStyle = styleGrelha(corLL, isBold: true);
else if (i >= 16 && i <= 18) cell.cellStyle = styleGrelha(corReb, isBold: true);
else cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
}
r++;
for(var p in processedPlayers) {
String minStr = '${p['minSecs'] ~/ 60}:${(p['minSecs'] % 60).toString().padLeft(2, '0')}';
String p2Pct = p['p2a'] > 0 ? '${((p['p2m'] / p['p2a']) * 100).toStringAsFixed(0)}%' : '-';
String p3Pct = p['p3a'] > 0 ? '${((p['p3m'] / p['p3a']) * 100).toStringAsFixed(0)}%' : '-';
String fgPct = p['fga'] > 0 ? '${((p['fgm'] / p['fga']) * 100).toStringAsFixed(0)}%' : '-';
String ftPct = p['fta'] > 0 ? '${((p['ftm'] / p['fta']) * 100).toStringAsFixed(0)}%' : '-';
List<CellValue> rowData = [
TextCellValue(p['num']), TextCellValue(p['name']), TextCellValue(minStr), IntCellValue(p['pts']),
IntCellValue(p['p2m']), IntCellValue(p['p2a']), TextCellValue(p2Pct),
IntCellValue(p['p3m']), IntCellValue(p['p3a']), TextCellValue(p3Pct),
IntCellValue(p['fgm']), IntCellValue(p['fga']), TextCellValue(fgPct),
IntCellValue(p['ftm']), IntCellValue(p['fta']), TextCellValue(ftPct),
IntCellValue(p['orb']), IntCellValue(p['drb']), IntCellValue(p['tr'])
];
for(int i = 0; i < rowData.length; i++) {
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
cell.value = rowData[i];
if (i == 1) cell.cellStyle = styleNome;
else if (i == 3) cell.cellStyle = styleGeralBold;
else cell.cellStyle = styleGeral;
}
r++;
}
String t2Pct = t2a > 0 ? '${((t2m / t2a) * 100).toStringAsFixed(0)}%' : '-';
String t3Pct = t3a > 0 ? '${((t3m / t3a) * 100).toStringAsFixed(0)}%' : '-';
String tFgPct = tFga > 0 ? '${((tFgm / tFga) * 100).toStringAsFixed(0)}%' : '-';
String tftPct = tfta > 0 ? '${((tftm / tfta) * 100).toStringAsFixed(0)}%' : '-';
String tMinStr = '${tMin ~/ 60}:${(tMin % 60).toString().padLeft(2, '0')}';
List<CellValue> totalRowT1 = [
TextCellValue(""), TextCellValue("TOTAL EQUIPA"), TextCellValue(tMinStr), IntCellValue(tPts),
IntCellValue(t2m), IntCellValue(t2a), TextCellValue(t2Pct),
IntCellValue(t3m), IntCellValue(t3a), TextCellValue(t3Pct),
IntCellValue(tFgm), IntCellValue(tFga), TextCellValue(tFgPct),
IntCellValue(tftm), IntCellValue(tfta), TextCellValue(tftPct),
IntCellValue(torb), IntCellValue(tdrb), IntCellValue(torb + tdrb)
];
for(int i = 0; i < totalRowT1.length; i++) {
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
cell.value = totalRowT1[i];
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
if (i >= 4 && i <= 6) cell.cellStyle = styleGrelha(cor2P, isBold: true);
else if (i >= 7 && i <= 9) cell.cellStyle = styleGrelha(cor3P, isBold: true);
else if (i >= 10 && i <= 12) cell.cellStyle = styleGrelha(corGlobal, isBold: true);
else if (i >= 13 && i <= 15) cell.cellStyle = styleGrelha(corLL, isBold: true);
else if (i >= 16 && i <= 18) cell.cellStyle = styleGrelha(corReb, isBold: true);
}
r += 3;
// TABELA 2: OUTRAS ESTATÍSTICAS
var secStart = CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r);
var secEnd = CellIndex.indexByColumnRow(columnIndex: 12, rowIndex: r);
sheet.merge(secStart, secEnd, customValue: TextCellValue("OUTRAS ESTATÍSTICAS: ${tName.toUpperCase()}"));
for(int i=0; i<=12; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleTituloSecundario;
r++;
List<String> colsT2 = ["", "NOME", "BR", "AS", "BP", "BLK", "FLS", "SO", "IL", "LI", "PA", "3S", "DR"];
for(int i = 0; i < colsT2.length; i++) {
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
cell.value = TextCellValue(colsT2[i]);
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
}
r++;
for(var p in processedPlayers) {
List<CellValue> rowData2 = [
TextCellValue(p['num']), TextCellValue(p['name']),
IntCellValue(p['stl']), IntCellValue(p['ast']), IntCellValue(p['tov']),
IntCellValue(p['blk']), IntCellValue(p['fls']), IntCellValue(p['so']),
IntCellValue(p['il']), IntCellValue(p['li']), IntCellValue(p['pa']),
IntCellValue(p['3s']), IntCellValue(p['dr'])
];
for(int i = 0; i < rowData2.length; i++) {
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
cell.value = rowData2[i];
cell.cellStyle = (i == 1) ? styleNome : styleGeral;
}
r++;
}
List<CellValue> totalRowT2 = [
TextCellValue(""), TextCellValue("TOTAL EQUIPA"),
IntCellValue(tStl), IntCellValue(tAst), IntCellValue(tTov), IntCellValue(tBlk), IntCellValue(tFls),
IntCellValue(tSo), IntCellValue(tIl), IntCellValue(tLi), IntCellValue(tPa), IntCellValue(tTresS), IntCellValue(tDr)
];
for(int i = 0; i < totalRowT2.length; i++) {
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
cell.value = totalRowT2[i];
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
}
r += 4;
}
// ── DESTAQUES DO JOGO ───────────────────────────────────────
if (gameData != null) {
var startD = CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r);
var endD = CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: r);
sheet.merge(startD, endD, customValue: TextCellValue("DESTAQUES DO JOGO"));
for(int i=0; i<=3; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleNomeEquipa;
r++;
void adicionarDestaque(String titulo, String valor) {
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r)).value = TextCellValue(titulo);
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r)).cellStyle = CellStyle(bold: true);
var sV = CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: r);
var eV = CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: r);
sheet.merge(sV, eV, customValue: TextCellValue(valor));
r++;
}
adicionarDestaque("Melhor Marcador:", gameData['top_pts_name'] ?? '---');
adicionarDestaque("Melhor Ressaltador:", gameData['top_rbs_name'] ?? '---');
adicionarDestaque("Melhor Passador:", gameData['top_ast_name'] ?? '---');
adicionarDestaque("MVP da Partida:", gameData['mvp_name'] ?? '---');
}
sheet.setColumnWidth(0, 18.0);
sheet.setColumnWidth(1, 26.0);
sheet.setColumnWidth(2, 8.0);
sheet.setColumnWidth(3, 6.0);
for(int i=4; i<=18; i++) sheet.setColumnWidth(i, 5.5);
var fileBytes = excel.save();
if (fileBytes != null) {
final directory = await getTemporaryDirectory();
String safeName = targetTeam == 'Ambas' ? '${myTeam}_vs_${opponentTeam}'.replaceAll(' ', '_') : targetTeam.replaceAll(' ', '_');
final filePath = '${directory.path}/BoxScore_$safeName.xlsx';
File(filePath)..createSync(recursive: true)..writeAsBytesSync(fileBytes);
await Share.shareXFiles([XFile(filePath)], text: 'Estatísticas do Jogo: $myTeam vs $opponentTeam');
}
} catch (e) {
debugPrint('Erro ao gerar Excel: $e');
}
}
}

View File

@@ -1,13 +1,15 @@
import 'package:supabase_flutter/supabase_flutter.dart';
import '../models/game_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:playmaker/pages/PlacarPage.dart'; import 'package:playmaker/pages/PlacarPage.dart';
import 'package:playmaker/classe/theme.dart'; import 'package:playmaker/classe/theme.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../controllers/game_controller.dart'; import '../controllers/game_controller.dart';
import '../models/game_model.dart';
import '../utils/size_extension.dart'; import '../utils/size_extension.dart';
import 'pdf_export_service.dart'; import 'pdf_export_service.dart';
import 'excel_export_service.dart';
class GameResultCard extends StatelessWidget { class GameResultCard extends StatelessWidget {
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season; final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
@@ -21,6 +23,67 @@ class GameResultCard extends StatelessWidget {
this.myTeamLogo, this.opponentTeamLogo, required this.sf, required this.onDelete, this.myTeamLogo, this.opponentTeamLogo, required this.sf, required this.onDelete,
}); });
void _showTeamSelectionDialog(BuildContext context, String format) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)),
title: Text('Gerar ${format.toUpperCase()}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf, color: Theme.of(context).colorScheme.onSurface)),
content: Text('De qual equipa pretende exportar as estatísticas?', style: TextStyle(fontSize: 14 * sf, color: Theme.of(context).colorScheme.onSurface)),
actions: [
TextButton(
onPressed: () {
Navigator.pop(ctx);
_exportDocument(context, format, myTeam);
},
child: Text(myTeam, style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf))
),
TextButton(
onPressed: () {
Navigator.pop(ctx);
_exportDocument(context, format, opponentTeam);
},
child: Text(opponentTeam, style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf))
),
TextButton(
onPressed: () {
Navigator.pop(ctx);
_exportDocument(context, format, 'Ambas');
},
child: Text('Ambas', style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * sf))
),
],
)
);
}
Future<void> _exportDocument(BuildContext context, String format, String targetTeam) async {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('A gerar ${format.toUpperCase()}...'), duration: const Duration(seconds: 1)));
if (format == 'pdf') {
await PdfExportService.generateAndPrintBoxScore(
gameId: gameId,
myTeam: myTeam,
opponentTeam: opponentTeam,
myScore: myScore,
opponentScore: opponentScore,
season: season,
targetTeam: targetTeam,
);
} else if (format == 'excel') {
await ExcelExportService.generateAndPrintBoxScoreExcel(
gameId: gameId,
myTeam: myTeam,
opponentTeam: opponentTeam,
myScore: myScore,
opponentScore: opponentScore,
season: season,
targetTeam: targetTeam,
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface; final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
@@ -46,32 +109,71 @@ class GameResultCard extends StatelessWidget {
], ],
), ),
// 👇 MENU DOS 3 PONTOS (MAIS NÍTIDO E MODERNO)
Positioned( Positioned(
top: -10 * sf, top: -12 * sf,
right: -10 * sf, right: -12 * sf,
child: Row( child: PopupMenuButton<String>(
children: [ icon: Icon(Icons.more_vert, color: Colors.grey.shade600, size: 26 * sf), // Ícone um pouco maior
IconButton( splashRadius: 24 * sf,
icon: Icon(Icons.picture_as_pdf, color: AppTheme.primaryRed.withOpacity(0.8), size: 22 * sf), elevation: 8, // Adiciona sombra para não se misturar com o fundo
splashRadius: 20 * sf, shadowColor: Colors.black45,
tooltip: 'Gerar PDF', shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16 * sf)),
onPressed: () async { color: Theme.of(context).colorScheme.surface,
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('A gerar PDF...'), duration: Duration(seconds: 1))); surfaceTintColor: Theme.of(context).colorScheme.surface, // Previne que o material 3 mude a cor
await PdfExportService.generateAndPrintBoxScore( onSelected: (value) {
gameId: gameId, if (value == 'pdf' || value == 'excel') {
myTeam: myTeam, _showTeamSelectionDialog(context, value);
opponentTeam: opponentTeam, } else if (value == 'delete') {
myScore: myScore, _showDeleteConfirmation(context);
opponentScore: opponentScore, }
season: season, },
); itemBuilder: (context) => [
}, PopupMenuItem(
value: 'pdf',
child: Row(
children: [
// Ícone com fundo arredondado
Container(
padding: EdgeInsets.all(8 * sf),
decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.1), shape: BoxShape.circle),
child: Icon(Icons.picture_as_pdf, color: AppTheme.primaryRed, size: 20 * sf),
),
SizedBox(width: 14 * sf),
Text('Gerar PDF', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
],
),
), ),
IconButton( PopupMenuItem(
icon: Icon(Icons.delete_outline, color: Colors.grey.shade400, size: 22 * sf), value: 'excel',
splashRadius: 20 * sf, child: Row(
tooltip: 'Eliminar Jogo', children: [
onPressed: () => _showDeleteConfirmation(context), // Ícone com fundo arredondado
Container(
padding: EdgeInsets.all(8 * sf),
decoration: BoxDecoration(color: Colors.green.shade600.withOpacity(0.1), shape: BoxShape.circle),
child: Icon(Icons.table_chart, color: Colors.green.shade600, size: 20 * sf),
),
SizedBox(width: 14 * sf),
Text('Gerar Excel', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
],
),
),
const PopupMenuDivider(height: 1),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
// Ícone com fundo arredondado
Container(
padding: EdgeInsets.all(8 * sf),
decoration: BoxDecoration(color: Colors.grey.shade500.withOpacity(0.1), shape: BoxShape.circle),
child: Icon(Icons.delete_outline, color: Colors.grey.shade700, size: 20 * sf),
),
SizedBox(width: 14 * sf),
Text('Eliminar Jogo', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
],
),
), ),
], ],
), ),

View File

@@ -23,6 +23,7 @@ class _HomeScreenState extends State<HomeScreen> {
final TeamController _teamController = TeamController(); final TeamController _teamController = TeamController();
String? _selectedTeamId; String? _selectedTeamId;
String _selectedTeamName = "Selecionar Equipa"; String _selectedTeamName = "Selecionar Equipa";
String? _selectedTeamLogo;
int _teamWins = 0; int _teamWins = 0;
int _teamLosses = 0; int _teamLosses = 0;
@@ -31,47 +32,113 @@ class _HomeScreenState extends State<HomeScreen> {
final _supabase = Supabase.instance.client; final _supabase = Supabase.instance.client;
String? _avatarUrl; String? _avatarUrl;
bool _isMemoryLoaded = false; // A variável mágica que impede o "piscar" inicial bool _isMemoryLoaded = false;
// A chave mágica para forçar a StatusPage a atualizar
String _statusKey = 'status_page_inicial';
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadUserAvatar(); _loadUserAvatar();
_loadSelectedTeam();
}
Future<void> _loadSelectedTeam() async {
final prefs = await SharedPreferences.getInstance();
final savedId = prefs.getString('last_team_id');
if (savedId != null && mounted) {
setState(() {
_selectedTeamId = savedId;
_selectedTeamName = prefs.getString('last_team_name') ?? "Selecionar Equipa";
_selectedTeamLogo = prefs.getString('last_team_logo');
_teamWins = prefs.getInt('last_team_wins') ?? 0;
_teamLosses = prefs.getInt('last_team_losses') ?? 0;
_teamDraws = prefs.getInt('last_team_draws') ?? 0;
});
}
final userId = _supabase.auth.currentUser?.id;
if (userId == null) return;
try {
final profile = await _supabase.from('profiles').select('selected_team_id').eq('id', userId).maybeSingle();
if (profile != null && profile['selected_team_id'] != null) {
final dbTeamId = profile['selected_team_id'].toString();
final teamData = await _supabase.from('teams').select().eq('id', dbTeamId).maybeSingle();
if (teamData != null && mounted) {
setState(() {
_selectedTeamId = teamData['id'].toString();
_selectedTeamName = teamData['name'] ?? 'Desconhecido';
_selectedTeamLogo = teamData['image_url'];
_teamWins = int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0;
_teamLosses = int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0;
_teamDraws = int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0;
});
await _saveToSharedPreferences();
}
}
} catch (e) {
debugPrint("Erro ao carregar equipa do Supabase: $e");
}
}
Future<void> _saveSelectedTeam() async {
await _saveToSharedPreferences();
final userId = _supabase.auth.currentUser?.id;
if (userId != null && _selectedTeamId != null) {
try {
await _supabase.from('profiles').upsert({
'id': userId,
'selected_team_id': _selectedTeamId,
});
} catch (e) {
debugPrint("Erro ao guardar equipa no Supabase: $e");
}
}
}
Future<void> _saveToSharedPreferences() async {
final prefs = await SharedPreferences.getInstance();
if (_selectedTeamId != null) {
await prefs.setString('last_team_id', _selectedTeamId!);
await prefs.setString('last_team_name', _selectedTeamName);
if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) {
await prefs.setString('last_team_logo', _selectedTeamLogo!);
} else {
await prefs.remove('last_team_logo');
}
await prefs.setInt('last_team_wins', _teamWins);
await prefs.setInt('last_team_losses', _teamLosses);
await prefs.setInt('last_team_draws', _teamDraws);
}
} }
// FUNÇÃO OTIMIZADA: Carrega da memória instantaneamente e atualiza em background
Future<void> _loadUserAvatar() async { Future<void> _loadUserAvatar() async {
// 1. LÊ DA MEMÓRIA RÁPIDA PRIMEIRO
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final savedUrl = prefs.getString('meu_avatar_guardado'); final savedUrl = prefs.getString('meu_avatar_guardado');
if (mounted) { if (mounted) {
setState(() { setState(() {
if (savedUrl != null) _avatarUrl = savedUrl; if (savedUrl != null) _avatarUrl = savedUrl;
_isMemoryLoaded = true; // Avisa o ecrã que a memória já respondeu! _isMemoryLoaded = true;
}); });
} }
// 2. VAI AO SUPABASE VERIFICAR SE TROCASTE DE FOTO
final userId = _supabase.auth.currentUser?.id; final userId = _supabase.auth.currentUser?.id;
if (userId == null) return; if (userId == null) return;
try { try {
final data = await _supabase final data = await _supabase.from('profiles').select('avatar_url').eq('id', userId).maybeSingle();
.from('profiles')
.select('avatar_url')
.eq('id', userId)
.maybeSingle();
if (mounted && data != null && data['avatar_url'] != null) { if (mounted && data != null && data['avatar_url'] != null) {
final urlDoSupabase = data['avatar_url']; final urlDoSupabase = data['avatar_url'];
// Se a foto na base de dados for nova, ele guarda e atualiza!
if (urlDoSupabase != savedUrl) { if (urlDoSupabase != savedUrl) {
await prefs.setString('meu_avatar_guardado', urlDoSupabase); await prefs.setString('meu_avatar_guardado', urlDoSupabase);
setState(() { setState(() { _avatarUrl = urlDoSupabase; });
_avatarUrl = urlDoSupabase;
});
} }
} }
} catch (e) { } catch (e) {
@@ -85,7 +152,7 @@ class _HomeScreenState extends State<HomeScreen> {
_buildHomeContent(context), _buildHomeContent(context),
const GamePage(), const GamePage(),
const TeamsPage(), const TeamsPage(),
const StatusPage(), StatusPage(key: ValueKey(_statusKey)), // A StatusPage recarrega sempre que a chave muda!
]; ];
return Scaffold( return Scaffold(
@@ -95,55 +162,37 @@ class _HomeScreenState extends State<HomeScreen> {
backgroundColor: AppTheme.primaryRed, backgroundColor: AppTheme.primaryRed,
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
leading: Padding( leading: Padding(
padding: EdgeInsets.all(10.0 * context.sf), padding: EdgeInsets.all(10.0 * context.sf),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(100), borderRadius: BorderRadius.circular(100),
onTap: () async { onTap: () async {
await Navigator.push( await Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen()));
context,
MaterialPageRoute(builder: (context) => const SettingsScreen()),
);
_loadUserAvatar(); _loadUserAvatar();
}, },
// SÓ MOSTRA A IMAGEM OU O BONECO DEPOIS DE LER A MEMÓRIA
child: !_isMemoryLoaded child: !_isMemoryLoaded
// Nos primeiros 0.05 segs, mostra só o círculo de fundo (sem boneco)
? CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)) ? CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2))
// Depois da memória responder:
: _avatarUrl != null && _avatarUrl!.isNotEmpty : _avatarUrl != null && _avatarUrl!.isNotEmpty
? CachedNetworkImage( ? CachedNetworkImage(
imageUrl: _avatarUrl!, imageUrl: _avatarUrl!,
fadeInDuration: Duration.zero, // Corta o atraso visual! fadeInDuration: Duration.zero,
imageBuilder: (context, imageProvider) => CircleAvatar( imageBuilder: (context, imageProvider) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), backgroundImage: imageProvider),
backgroundColor: Colors.white.withOpacity(0.2),
backgroundImage: imageProvider,
),
placeholder: (context, url) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)), placeholder: (context, url) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)),
errorWidget: (context, url, error) => CircleAvatar( errorWidget: (context, url, error) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf)),
backgroundColor: Colors.white.withOpacity(0.2),
child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf),
),
) )
// Se não tiver foto nenhuma, aí sim mostra o boneco : CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf)),
: CircleAvatar(
backgroundColor: Colors.white.withOpacity(0.2),
child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf),
),
), ),
), ),
), ),
body: IndexedStack(index: _selectedIndex, children: pages),
body: IndexedStack(
index: _selectedIndex,
children: pages,
),
bottomNavigationBar: NavigationBar( bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex, selectedIndex: _selectedIndex,
onDestinationSelected: (index) => setState(() => _selectedIndex = index), onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
if (index == 0) {
_loadSelectedTeam();
}
},
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
elevation: 1, elevation: 1,
@@ -167,13 +216,8 @@ class _HomeScreenState extends State<HomeScreen> {
return StreamBuilder<List<Map<String, dynamic>>>( return StreamBuilder<List<Map<String, dynamic>>>(
stream: _teamController.teamsStream, stream: _teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
// Correção: Verifica hasData para evitar piscar tela de loading if (!snapshot.hasData && snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
if (!snapshot.hasData && snapshot.connectionState == ConnectionState.waiting) { if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface))));
return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface))));
}
final teams = snapshot.data!; final teams = snapshot.data!;
return ListView.builder( return ListView.builder(
@@ -181,18 +225,33 @@ class _HomeScreenState extends State<HomeScreen> {
itemCount: teams.length, itemCount: teams.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final team = teams[index]; final team = teams[index];
final String? logoUrl = team['image_url'];
return ListTile( return ListTile(
leading: const Icon(Icons.shield, color: AppTheme.primaryRed), leading: ClipOval(
child: Container(
width: 36 * context.sf, height: 36 * context.sf, color: AppTheme.primaryRed.withOpacity(0.1),
child: (logoUrl != null && logoUrl.isNotEmpty)
? CachedNetworkImage(imageUrl: logoUrl, fit: BoxFit.cover, placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf))
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
),
),
title: Text(team['name'] ?? 'Sem Nome', style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)), title: Text(team['name'] ?? 'Sem Nome', style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
onTap: () { onTap: () async {
setState(() { setState(() {
_selectedTeamId = team['id'].toString(); _selectedTeamId = team['id'].toString();
_selectedTeamName = team['name'] ?? 'Desconhecido'; _selectedTeamName = team['name'] ?? 'Desconhecido';
_selectedTeamLogo = logoUrl;
_teamWins = int.tryParse(team['wins']?.toString() ?? '0') ?? 0; _teamWins = int.tryParse(team['wins']?.toString() ?? '0') ?? 0;
_teamLosses = int.tryParse(team['losses']?.toString() ?? '0') ?? 0; _teamLosses = int.tryParse(team['losses']?.toString() ?? '0') ?? 0;
_teamDraws = int.tryParse(team['draws']?.toString() ?? '0') ?? 0; _teamDraws = int.tryParse(team['draws']?.toString() ?? '0') ?? 0;
// Dizemos à StatusPage que a equipa mudou alterando a chave!
_statusKey = DateTime.now().toString();
}); });
Navigator.pop(context);
await _saveSelectedTeam();
if (context.mounted) Navigator.pop(context);
}, },
); );
}, },
@@ -225,16 +284,14 @@ class _HomeScreenState extends State<HomeScreen> {
onTap: () => _showTeamSelector(context), onTap: () => _showTeamSelector(context),
child: Container( child: Container(
padding: EdgeInsets.all(12 * context.sf), padding: EdgeInsets.all(12 * context.sf),
decoration: BoxDecoration( decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))),
color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(15 * context.sf),
border: Border.all(color: Colors.grey.withOpacity(0.2))
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row(children: [ Row(children: [
Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf), (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty)
? ClipOval(child: CachedNetworkImage(imageUrl: _selectedTeamLogo!, width: 24 * context.sf, height: 24 * context.sf, fit: BoxFit.cover, placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf), errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf)))
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
SizedBox(width: 10 * context.sf), SizedBox(width: 10 * context.sf),
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor)) Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
]), ]),
@@ -263,17 +320,7 @@ class _HomeScreenState extends State<HomeScreen> {
children: [ children: [
Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statRebBg)), Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statRebBg)),
SizedBox(width: 12 * context.sf), SizedBox(width: 12 * context.sf),
Expanded( Expanded(child: PieChartCard(victories: _teamWins, defeats: _teamLosses, draws: _teamDraws, title: 'DESEMPENHO', subtitle: 'Temporada', backgroundColor: AppTheme.statPieBg, sf: context.sf)),
child: PieChartCard(
victories: _teamWins,
defeats: _teamLosses,
draws: _teamDraws,
title: 'DESEMPENHO',
subtitle: 'Temporada',
backgroundColor: AppTheme.statPieBg,
sf: context.sf
),
),
], ],
), ),
), ),
@@ -284,45 +331,16 @@ class _HomeScreenState extends State<HomeScreen> {
_selectedTeamName == "Selecionar Equipa" _selectedTeamName == "Selecionar Equipa"
? Container( ? Container(
width: double.infinity, width: double.infinity, padding: EdgeInsets.all(24.0 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color ?? Colors.white, borderRadius: BorderRadius.circular(16 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4))]),
padding: EdgeInsets.all(24.0 * context.sf),
decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color ?? Colors.white,
borderRadius: BorderRadius.circular(16 * context.sf),
border: Border.all(color: Colors.grey.withOpacity(0.1)),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4))],
),
child: Column( child: Column(
children: [ children: [
Container( Container(padding: EdgeInsets.all(18 * context.sf), decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.08), shape: BoxShape.circle), child: Icon(Icons.shield_outlined, color: AppTheme.primaryRed, size: 42 * context.sf)),
padding: EdgeInsets.all(18 * context.sf),
decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.08), shape: BoxShape.circle),
child: Icon(Icons.shield_outlined, color: AppTheme.primaryRed, size: 42 * context.sf),
),
SizedBox(height: 20 * context.sf), SizedBox(height: 20 * context.sf),
Text("Nenhuma Equipa Ativa", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: textColor)), Text("Nenhuma Equipa Ativa", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: textColor)),
SizedBox(height: 8 * context.sf), SizedBox(height: 8 * context.sf),
Text( Text("Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.", textAlign: TextAlign.center, style: TextStyle(fontSize: 13 * context.sf, color: Colors.grey.shade600, height: 1.4)),
"Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13 * context.sf, color: Colors.grey.shade600, height: 1.4),
),
SizedBox(height: 24 * context.sf), SizedBox(height: 24 * context.sf),
SizedBox( SizedBox(width: double.infinity, height: 48 * context.sf, child: ElevatedButton.icon(onPressed: () => _showTeamSelector(context), style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), icon: Icon(Icons.touch_app, size: 20 * context.sf), label: Text("Selecionar Agora", style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.bold)))),
width: double.infinity,
height: 48 * context.sf,
child: ElevatedButton.icon(
onPressed: () => _showTeamSelector(context),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryRed,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf)),
),
icon: Icon(Icons.touch_app, size: 20 * context.sf),
label: Text("Selecionar Agora", style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.bold)),
),
),
], ],
), ),
) )
@@ -330,11 +348,7 @@ class _HomeScreenState extends State<HomeScreen> {
stream: _supabase.from('games').stream(primaryKey: ['id']).order('game_date', ascending: false), stream: _supabase.from('games').stream(primaryKey: ['id']).order('game_date', ascending: false),
builder: (context, gameSnapshot) { builder: (context, gameSnapshot) {
if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red)); if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
if (!gameSnapshot.hasData && gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
// Correção: Verifica hasData em vez de ConnectionState para manter a lista na tela enquanto atualiza em plano de fundo
if (!gameSnapshot.hasData && gameSnapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final todosOsJogos = gameSnapshot.data ?? []; final todosOsJogos = gameSnapshot.data ?? [];
final gamesList = todosOsJogos.where((game) { final gamesList = todosOsJogos.where((game) {
@@ -344,44 +358,19 @@ class _HomeScreenState extends State<HomeScreen> {
return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado'; return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado';
}).take(3).toList(); }).take(3).toList();
if (gamesList.isEmpty) { if (gamesList.isEmpty) return Container(width: double.infinity, padding: EdgeInsets.all(20 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(14)), alignment: Alignment.center, child: const Text("Ainda não há jogos terminados.", style: TextStyle(color: Colors.grey)));
return Container(
width: double.infinity,
padding: EdgeInsets.all(20 * context.sf),
decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(14)),
alignment: Alignment.center,
child: const Text("Ainda não há jogos terminados.", style: TextStyle(color: Colors.grey)),
);
}
return Column( return Column(
children: gamesList.map((game) { children: gamesList.map((game) {
String dbMyTeam = game['my_team']?.toString() ?? ''; String dbMyTeam = game['my_team']?.toString() ?? ''; String dbOppTeam = game['opponent_team']?.toString() ?? '';
String dbOppTeam = game['opponent_team']?.toString() ?? ''; int dbMyScore = int.tryParse(game['my_score']?.toString() ?? '0') ?? 0; int dbOppScore = int.tryParse(game['opponent_score']?.toString() ?? '0') ?? 0;
int dbMyScore = int.tryParse(game['my_score']?.toString() ?? '0') ?? 0;
int dbOppScore = int.tryParse(game['opponent_score']?.toString() ?? '0') ?? 0;
String opponent; int myScore; int oppScore; String opponent; int myScore; int oppScore;
if (dbMyTeam == _selectedTeamName) { if (dbMyTeam == _selectedTeamName) { opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore; } else { opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore; }
opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore; String rawDate = game['game_date']?.toString() ?? '---'; String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
} else { String result = myScore > oppScore ? 'V' : (myScore < oppScore ? 'D' : 'E');
opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore;
}
String rawDate = game['game_date']?.toString() ?? '---'; return _buildGameHistoryCard(context: context, opponent: opponent, result: result, myScore: myScore, oppScore: oppScore, date: date, topPts: game['top_pts_name'] ?? '---', topAst: game['top_ast_name'] ?? '---', topRbs: game['top_rbs_name'] ?? '---', topDef: game['top_def_name'] ?? '---', mvp: game['mvp_name'] ?? '---');
String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
String result = 'E';
if (myScore > oppScore) result = 'V';
if (myScore < oppScore) result = 'D';
return _buildGameHistoryCard(
context: context, opponent: opponent, result: result,
myScore: myScore, oppScore: oppScore, date: date,
topPts: game['top_pts_name'] ?? '---', topAst: game['top_ast_name'] ?? '---',
topRbs: game['top_rbs_name'] ?? '---', topDef: game['top_def_name'] ?? '---', mvp: game['mvp_name'] ?? '---',
);
}).toList(), }).toList(),
); );
}, },
@@ -404,39 +393,20 @@ class _HomeScreenState extends State<HomeScreen> {
astMap[pid] = (astMap[pid] ?? 0) + (int.tryParse(row['ast']?.toString() ?? '0') ?? 0); astMap[pid] = (astMap[pid] ?? 0) + (int.tryParse(row['ast']?.toString() ?? '0') ?? 0);
rbsMap[pid] = (rbsMap[pid] ?? 0) + (int.tryParse(row['rbs']?.toString() ?? '0') ?? 0); rbsMap[pid] = (rbsMap[pid] ?? 0) + (int.tryParse(row['rbs']?.toString() ?? '0') ?? 0);
} }
if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0};
if (ptsMap.isEmpty) { String getBest(Map<String, int> map) { if (map.isEmpty) return '---'; return namesMap[map.entries.reduce((a, b) => a.value > b.value ? a : b).key] ?? '---'; }
return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0}; int getBestVal(Map<String, int> map) { if (map.isEmpty) return 0; return map.values.reduce((a, b) => a > b ? a : b); }
} return {'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap), 'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap), 'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)};
String getBest(Map<String, int> map) {
if (map.isEmpty) return '---';
var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key;
return namesMap[bestId] ?? '---';
}
int getBestVal(Map<String, int> map) {
if (map.isEmpty) return 0;
return map.values.reduce((a, b) => a > b ? a : b);
}
return {
'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap),
'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap),
'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)
};
} }
Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) { Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) {
return Card( return Card(
elevation: 4, margin: EdgeInsets.zero, elevation: 4, margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: AppTheme.warningAmber, width: 2) : BorderSide.none),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: AppTheme.warningAmber, width: 2) : BorderSide.none),
child: Container( child: Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])), decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final double ch = constraints.maxHeight; final double ch = constraints.maxHeight; final double cw = constraints.maxWidth;
final double cw = constraints.maxWidth;
return Padding( return Padding(
padding: EdgeInsets.all(cw * 0.06), padding: EdgeInsets.all(cw * 0.06),
child: Column( child: Column(
@@ -444,23 +414,13 @@ class _HomeScreenState extends State<HomeScreen> {
children: [ children: [
Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis), Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis),
SizedBox(height: ch * 0.011), SizedBox(height: ch * 0.011),
SizedBox( SizedBox(width: double.infinity, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)))),
width: double.infinity,
child: FittedBox(
fit: BoxFit.scaleDown, alignment: Alignment.centerLeft,
child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)),
),
),
const Spacer(), const Spacer(),
Center(child: FittedBox(fit: BoxFit.scaleDown, child: Text(statValue, style: TextStyle(fontSize: ch * 0.18, fontWeight: FontWeight.bold, color: Colors.white, height: 1.0)))), Center(child: FittedBox(fit: BoxFit.scaleDown, child: Text(statValue, style: TextStyle(fontSize: ch * 0.18, fontWeight: FontWeight.bold, color: Colors.white, height: 1.0)))),
SizedBox(height: ch * 0.015), SizedBox(height: ch * 0.015),
Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))), Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))),
const Spacer(), const Spacer(),
Container( Container(width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035), decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)), child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold)))),
width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035),
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)),
child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold)))
),
], ],
), ),
); );
@@ -470,33 +430,20 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
Widget _buildGameHistoryCard({ Widget _buildGameHistoryCard({required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date, required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp}) {
required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date, bool isWin = result == 'V'; bool isDraw = result == 'E';
required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp
}) {
bool isWin = result == 'V';
bool isDraw = result == 'E';
Color statusColor = isWin ? AppTheme.successGreen : (isDraw ? AppTheme.warningAmber : AppTheme.oppTeamRed); Color statusColor = isWin ? AppTheme.successGreen : (isDraw ? AppTheme.warningAmber : AppTheme.oppTeamRed);
final bgColor = Theme.of(context).cardTheme.color; final bgColor = Theme.of(context).cardTheme.color; final textColor = Theme.of(context).colorScheme.onSurface;
final textColor = Theme.of(context).colorScheme.onSurface;
return Container( return Container(
margin: EdgeInsets.only(bottom: 14 * context.sf), margin: EdgeInsets.only(bottom: 14 * context.sf), decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))]),
decoration: BoxDecoration(
color: bgColor, borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))],
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: EdgeInsets.all(14 * context.sf), padding: EdgeInsets.all(14 * context.sf),
child: Row( child: Row(
children: [ children: [
Container( Container(width: 36 * context.sf, height: 36 * context.sf, decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle), child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)))),
width: 36 * context.sf, height: 36 * context.sf,
decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle),
child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf))),
),
SizedBox(width: 14 * context.sf), SizedBox(width: 14 * context.sf),
Expanded( Expanded(
child: Column( child: Column(
@@ -508,14 +455,7 @@ class _HomeScreenState extends State<HomeScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), maxLines: 1, overflow: TextOverflow.ellipsis)), Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), maxLines: 1, overflow: TextOverflow.ellipsis)),
Padding( Padding(padding: EdgeInsets.symmetric(horizontal: 8 * context.sf), child: Container(padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), decoration: BoxDecoration(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), borderRadius: BorderRadius.circular(8)), child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: textColor)))),
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), borderRadius: BorderRadius.circular(8)),
child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: textColor)),
),
),
Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)), Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)),
], ],
), ),
@@ -527,30 +467,14 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
Divider(height: 1, color: Colors.grey.withOpacity(0.1), thickness: 1.5), Divider(height: 1, color: Colors.grey.withOpacity(0.1), thickness: 1.5),
Container( Container(
width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf), width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))),
child: Column( child: Column(
children: [ children: [
Row( Row(children: [Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)), Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef))]),
children: [
Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)),
Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef)),
],
),
SizedBox(height: 8 * context.sf), SizedBox(height: 8 * context.sf),
Row( Row(children: [Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)), Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs))]),
children: [
Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)),
Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs)),
],
),
SizedBox(height: 8 * context.sf), SizedBox(height: 8 * context.sf),
Row( Row(children: [Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)), const Expanded(child: SizedBox())]),
children: [
Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)),
const Expanded(child: SizedBox()),
],
),
], ],
), ),
) )
@@ -562,20 +486,9 @@ class _HomeScreenState extends State<HomeScreen> {
Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, String label, String value, {bool isMvp = false}) { Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, String label, String value, {bool isMvp = false}) {
return Row( return Row(
children: [ children: [
Icon(icon, size: 14 * context.sf, color: color), Icon(icon, size: 14 * context.sf, color: color), SizedBox(width: 4 * context.sf),
SizedBox(width: 4 * context.sf),
Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)), Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)),
Expanded( Expanded(child: Text(value, style: TextStyle(fontSize: 11 * context.sf, color: isMvp ? AppTheme.warningAmber : Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)),
child: Text(
value,
style: TextStyle(
fontSize: 11 * context.sf,
color: isMvp ? AppTheme.warningAmber : Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold
),
maxLines: 1, overflow: TextOverflow.ellipsis
)
),
], ],
); );
} }

View File

@@ -4,7 +4,6 @@ import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart'; import 'package:printing/printing.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
// Modelo local para os tiros
class _ShotDot { class _ShotDot {
final double relX; final double relX;
final double relY; final double relY;
@@ -13,10 +12,6 @@ class _ShotDot {
} }
class PdfExportService { class PdfExportService {
// ════════════════════════════════════════════════════════════════════════════
// ENTRY POINT
// ════════════════════════════════════════════════════════════════════════════
static Future<void> generateAndPrintBoxScore({ static Future<void> generateAndPrintBoxScore({
required String gameId, required String gameId,
required String myTeam, required String myTeam,
@@ -24,18 +19,15 @@ class PdfExportService {
required String myScore, required String myScore,
required String opponentScore, required String opponentScore,
required String season, required String season,
required String targetTeam,
}) async { }) async {
final supabase = Supabase.instance.client; final supabase = Supabase.instance.client;
// ── Jogo ──────────────────────────────────────────────────────────────── // ── Jogo ────────────────────────────────────────────────────────────────
final gameData = final gameData = await supabase.from('games').select().eq('id', gameId).single();
await supabase.from('games').select().eq('id', gameId).single();
// ── Equipas ───────────────────────────────────────────────────────────── // ── Equipas ─────────────────────────────────────────────────────────────
final teamsData = await supabase final teamsData = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
.from('teams')
.select('id, name')
.inFilter('name', [myTeam, opponentTeam]);
String? myTeamId; String? myTeamId;
for (var t in teamsData) { for (var t in teamsData) {
@@ -44,32 +36,19 @@ class PdfExportService {
// ── Jogadores (Apenas a minha equipa) ─────────────────────────────────── // ── Jogadores (Apenas a minha equipa) ───────────────────────────────────
List<dynamic> myPlayers = myTeamId != null List<dynamic> myPlayers = myTeamId != null
? await supabase ? await supabase.from('members').select().eq('team_id', myTeamId).eq('type', 'Jogador')
.from('members')
.select()
.eq('team_id', myTeamId)
.eq('type', 'Jogador')
: []; : [];
// ── Estatísticas ───────────────────────────────────────────────────────── // ── Estatísticas ─────────────────────────────────────────────────────────
final statsData = final statsData = await supabase.from('player_stats').select().eq('game_id', gameId);
await supabase.from('player_stats').select().eq('game_id', gameId);
Map<String, Map<String, dynamic>> statsMap = {}; Map<String, Map<String, dynamic>> statsMap = {};
for (var s in statsData) { for (var s in statsData) {
statsMap[s['member_id'].toString()] = s; statsMap[s['member_id'].toString()] = s;
} }
// ── Tiros (para o mapa de calor da minha equipa) ────────────────────── // ── Tiros ──────────────────────
final shotsData = await supabase final shotsData = await supabase.from('shot_locations').select().eq('game_id', gameId);
.from('shot_locations') final Set<String> myPlayerIds = myPlayers.map((p) => p['id'].toString()).toSet();
.select()
.eq('game_id', gameId);
// IDs da minha equipa
final Set<String> myPlayerIds =
myPlayers.map((p) => p['id'].toString()).toSet();
// Separa os tiros: todos da minha equipa, depois por jogador
final List<_ShotDot> myTeamShots = []; final List<_ShotDot> myTeamShots = [];
final Map<String, List<_ShotDot>> shotsByPlayer = {}; final Map<String, List<_ShotDot>> shotsByPlayer = {};
@@ -86,16 +65,14 @@ class PdfExportService {
shotsByPlayer.putIfAbsent(memberId, () => []).add(dot); shotsByPlayer.putIfAbsent(memberId, () => []).add(dot);
} }
// ── Tabela de estatísticas (Apenas a minha equipa) ──────────────────── // ── Tabela de estatísticas ────────────────────
List<List<String>> myTeamTable = List<List<String>> myTeamTable = _buildTeamTableData(myPlayers, statsMap);
_buildTeamTableData(myPlayers, statsMap);
// ════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════
// CONSTRUÇÃO DO PDF // CONSTRUÇÃO DO PDF
// ════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════
final pdf = pw.Document(); final pdf = pw.Document();
// ── PÁGINA 1: Box Score ──────────────────────────────────────────────
pdf.addPage( pdf.addPage(
pw.Page( pw.Page(
pageFormat: PdfPageFormat.a4.landscape, pageFormat: PdfPageFormat.a4.landscape,
@@ -110,71 +87,81 @@ class PdfExportService {
children: [ children: [
pw.Row( pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [ children: [
pw.Text('Relatório Estatístico', pw.Column(
style: pw.TextStyle( crossAxisAlignment: pw.CrossAxisAlignment.start,
fontSize: 22, children: [
fontWeight: pw.FontWeight.bold)), pw.Text('Relatório Estatístico', style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold)),
pw.SizedBox(height: 10),
pw.Text('Equipa: $myTeam', style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold, color: const PdfColor.fromInt(0xFFA00000))),
]
),
pw.Column( pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end, crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [ children: [
pw.Text('$myTeam vs $opponentTeam', pw.Text('$myTeam vs $opponentTeam', style: pw.TextStyle(fontSize: 15, fontWeight: pw.FontWeight.bold)),
style: pw.TextStyle( pw.Text('Resultado: $myScore$opponentScore', style: const pw.TextStyle(fontSize: 13)),
fontSize: 15, pw.Text('Época: $season', style: const pw.TextStyle(fontSize: 11)),
fontWeight: pw.FontWeight.bold)), pw.SizedBox(height: 10),
pw.Text('Resultado: $myScore$opponentScore',
style: const pw.TextStyle(fontSize: 13)), // 👇 NOVA TABELA: PONTUAÇÃO POR PERÍODO 👇
pw.Text('Época: $season', pw.Table.fromTextArray(
style: const pw.TextStyle(fontSize: 11)), context: context,
border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 8),
cellStyle: const pw.TextStyle(fontSize: 8),
headerDecoration: const pw.BoxDecoration(color: PdfColors.grey200),
cellAlignment: pw.Alignment.center,
data: <List<String>>[
['Equipa', '1ºQ', '2ºQ', '3ºQ', '4ºQ', 'F'],
[
myTeam,
gameData['my_q1']?.toString() ?? '-',
gameData['my_q2']?.toString() ?? '-',
gameData['my_q3']?.toString() ?? '-',
gameData['my_q4']?.toString() ?? '-',
myScore
],
[
opponentTeam,
gameData['opp_q1']?.toString() ?? '-',
gameData['opp_q2']?.toString() ?? '-',
gameData['opp_q3']?.toString() ?? '-',
gameData['opp_q4']?.toString() ?? '-',
opponentScore
],
],
)
], ],
), ),
], ],
), ),
pw.SizedBox(height: 12),
pw.Text('Equipa: $myTeam',
style: pw.TextStyle(
fontSize: 14,
fontWeight: pw.FontWeight.bold,
color: const PdfColor.fromInt(0xFFA00000))),
pw.SizedBox(height: 8), pw.SizedBox(height: 8),
pw.Text('Pontos e Lançamentos', pw.Text('Pontos e Lançamentos', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
style: pw.TextStyle(
fontSize: 10,
fontWeight: pw.FontWeight.bold,
color: PdfColors.grey700)),
pw.SizedBox(height: 2), pw.SizedBox(height: 2),
_buildPdfTablePart1( _buildPdfTablePart1(myTeamTable, const PdfColor.fromInt(0xFFA00000)),
myTeamTable, const PdfColor.fromInt(0xFFA00000)),
pw.SizedBox(height: 14), pw.SizedBox(height: 14),
pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)', pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
style: pw.TextStyle(
fontSize: 10,
fontWeight: pw.FontWeight.bold,
color: PdfColors.grey700)),
pw.SizedBox(height: 2), pw.SizedBox(height: 2),
_buildPdfTablePart2( _buildPdfTablePart2(myTeamTable, const PdfColor.fromInt(0xFFA00000)),
myTeamTable, const PdfColor.fromInt(0xFFA00000)),
pw.SizedBox(height: 16), pw.SizedBox(height: 16),
pw.Row( pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start, crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [ children: [
_buildSummaryBox('Melhor Marcador', _buildSummaryBox('Melhor Marcador', gameData['top_pts_name'] ?? '---'),
gameData['top_pts_name'] ?? '---'),
pw.SizedBox(width: 10), pw.SizedBox(width: 10),
_buildSummaryBox('Melhor Ressaltador', _buildSummaryBox('Melhor Ressaltador', gameData['top_rbs_name'] ?? '---'),
gameData['top_rbs_name'] ?? '---'),
pw.SizedBox(width: 10), pw.SizedBox(width: 10),
_buildSummaryBox('Melhor Passador', _buildSummaryBox('Melhor Passador', gameData['top_ast_name'] ?? '---'),
gameData['top_ast_name'] ?? '---'),
pw.SizedBox(width: 10), pw.SizedBox(width: 10),
_buildSummaryBox( _buildSummaryBox('MVP', gameData['mvp_name'] ?? '---'),
'MVP', gameData['mvp_name'] ?? '---'),
], ],
), ),
], ],
@@ -195,15 +182,13 @@ class PdfExportService {
return pw.Column( return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start, crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [ children: [
_heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)', _heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)', const PdfColor.fromInt(0xFFA00000)),
const PdfColor.fromInt(0xFFA00000)),
pw.SizedBox(height: 12), pw.SizedBox(height: 12),
pw.Expanded( pw.Expanded(
child: pw.Center( child: pw.Center(
child: pw.CustomPaint( child: pw.CustomPaint(
size: const PdfPoint(360, 360), size: const PdfPoint(360, 360),
painter: (canvas, size) => painter: (canvas, size) => _paintCourt(canvas, size, myTeamShots),
_paintCourt(canvas, size, myTeamShots),
), ),
), ),
), ),
@@ -217,7 +202,6 @@ class PdfExportService {
} }
// ── PÁGINAS 3+: Mapa de Calor por Jogador (4 por folha) ────────────── // ── PÁGINAS 3+: Mapa de Calor por Jogador (4 por folha) ──────────────
// 👇 FILTRO ATIVO: Só entra aqui quem tiver tiros na lista "shotsByPlayer"!
final activePlayers = myPlayers.where((p) { final activePlayers = myPlayers.where((p) {
final pid = p['id'].toString(); final pid = p['id'].toString();
return shotsByPlayer[pid] != null && shotsByPlayer[pid]!.isNotEmpty; return shotsByPlayer[pid] != null && shotsByPlayer[pid]!.isNotEmpty;
@@ -280,9 +264,6 @@ class PdfExportService {
); );
} }
// ════════════════════════════════════════════════════════════════════════════
// WIDGET DO MAPA DE CALOR INDIVIDUAL
// ════════════════════════════════════════════════════════════════════════════
static pw.Widget _buildPlayerHeatmap(dynamic player, List<_ShotDot> shots, Map<String, dynamic> stats) { static pw.Widget _buildPlayerHeatmap(dynamic player, List<_ShotDot> shots, Map<String, dynamic> stats) {
final String playerName = player['name']?.toString() ?? 'Jogador'; final String playerName = player['name']?.toString() ?? 'Jogador';
final String playerNumber = player['number']?.toString() ?? '0'; final String playerNumber = player['number']?.toString() ?? '0';
@@ -320,16 +301,11 @@ class PdfExportService {
); );
} }
// ════════════════════════════════════════════════════════════════════════════
// CORREÇÃO: DESENHO DO CAMPO E LINHAS ADAPTADAS
// ════════════════════════════════════════════════════════════════════════════
static void _paintCourt(PdfGraphics canvas, PdfPoint size, List<_ShotDot> shots) { static void _paintCourt(PdfGraphics canvas, PdfPoint size, List<_ShotDot> shots) {
final double w = size.x; final double w = size.x;
final double h = size.y; final double h = size.y;
final double basketX = w / 2; final double basketX = w / 2;
// Fundo Amarelo (Toda a área)
canvas canvas
..setFillColor(const PdfColor.fromInt(0xFFDFAB00)) ..setFillColor(const PdfColor.fromInt(0xFFDFAB00))
..drawRect(0, 0, w, h) ..drawRect(0, 0, w, h)
@@ -341,7 +317,6 @@ class PdfExportService {
final double alturaDoArco = larguraDoArco * 0.30; final double alturaDoArco = larguraDoArco * 0.30;
final double totalArcoHeight = alturaDoArco * 4; final double totalArcoHeight = alturaDoArco * 4;
// ── 1. LINHAS BRANCAS ───────────────────────────────────────────────
canvas.setStrokeColor(PdfColors.white); canvas.setStrokeColor(PdfColors.white);
canvas.setLineWidth(2.0); canvas.setLineWidth(2.0);
@@ -350,7 +325,6 @@ class PdfExportService {
_drawLine(canvas, h, 0, length, margin, length); _drawLine(canvas, h, 0, length, margin, length);
_drawLine(canvas, h, w - margin, length, w, length); _drawLine(canvas, h, w - margin, length, w, length);
// Arco 3pts
_drawEllipseArc(canvas, h, basketX, length, larguraDoArco, totalArcoHeight / 2, 0, math.pi); _drawEllipseArc(canvas, h, basketX, length, larguraDoArco, totalArcoHeight / 2, 0, math.pi);
double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75)); double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75));
@@ -361,46 +335,34 @@ class PdfExportService {
_drawLine(canvas, h, sXL, sYL, 0, h * 0.85); _drawLine(canvas, h, sXL, sYL, 0, h * 0.85);
_drawLine(canvas, h, sXR, sYR, w, h * 0.85); _drawLine(canvas, h, sXR, sYR, w, h * 0.85);
// ── 2. LINHAS PRETAS ─────────────────────────────────────────────────
canvas.setStrokeColor(PdfColors.black); canvas.setStrokeColor(PdfColors.black);
canvas.setLineWidth(1.5); canvas.setLineWidth(1.5);
final double pW = w * 0.28; final double pW = w * 0.28;
final double pH = h * 0.38; final double pH = h * 0.38;
// Garrafão
_drawRect(canvas, h, basketX - pW / 2, 0, pW, pH); _drawRect(canvas, h, basketX - pW / 2, 0, pW, pH);
// Círculo Lances Livres
final double ftR = pW / 2; final double ftR = pW / 2;
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, 0, math.pi); _drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, 0, math.pi);
// Tracejado
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, math.pi + (i * 2 * (math.pi / 20)), math.pi / 20); _drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, math.pi + (i * 2 * (math.pi / 20)), math.pi / 20);
} }
// Linhas oblíquas do garrafão
_drawLine(canvas, h, basketX - pW / 2, pH, sXL, sYL); _drawLine(canvas, h, basketX - pW / 2, pH, sXL, sYL);
_drawLine(canvas, h, basketX + pW / 2, pH, sXR, sYR); _drawLine(canvas, h, basketX + pW / 2, pH, sXR, sYR);
// Meio Campo
_drawEllipseArc(canvas, h, basketX, h, w * 0.12, w * 0.12, math.pi, math.pi); _drawEllipseArc(canvas, h, basketX, h, w * 0.12, w * 0.12, math.pi, math.pi);
// Cesto e Tabela
_drawCircle(canvas, h, basketX, h * 0.12, w * 0.02); _drawCircle(canvas, h, basketX, h * 0.12, w * 0.02);
_drawLine(canvas, h, basketX - w * 0.08, h * 0.12 - 5, basketX + w * 0.08, h * 0.12 - 5); _drawLine(canvas, h, basketX - w * 0.08, h * 0.12 - 5, basketX + w * 0.08, h * 0.12 - 5);
// ── 3. TIROS ─────────────────────────────────────────────────────────
for (final shot in shots) { for (final shot in shots) {
final double px = shot.relX * w; final double px = shot.relX * w;
final double py = shot.relY * h; final double py = shot.relY * h;
final PdfColor dotColor = shot.isMake ? PdfColors.green600 : PdfColors.red600; final PdfColor dotColor = shot.isMake ? PdfColors.green600 : PdfColors.red600;
// Desenha Círculo Colorido
_fillCircle(canvas, h, px, py, 6, dotColor); _fillCircle(canvas, h, px, py, 6, dotColor);
// Símbolos
canvas.setStrokeColor(PdfColors.white); canvas.setStrokeColor(PdfColors.white);
canvas.setLineWidth(1.2); canvas.setLineWidth(1.2);
if (shot.isMake) { if (shot.isMake) {
@@ -413,19 +375,12 @@ class PdfExportService {
} }
} }
// ── Helpers com inversão automática do Eixo Y para casar com Flutter ──
static void _drawLine(PdfGraphics c, double canvasH, double x1, double y1, double x2, double y2) { static void _drawLine(PdfGraphics c, double canvasH, double x1, double y1, double x2, double y2) {
c.moveTo(x1, canvasH - y1); c.moveTo(x1, canvasH - y1);
c.lineTo(x2, canvasH - y2); c.lineTo(x2, canvasH - y2);
c.strokePath(); c.strokePath();
} }
static void _lineRaw(PdfGraphics c, double x1, double y1, double x2, double y2) {
c.moveTo(x1, y1);
c.lineTo(x2, y2);
c.strokePath();
}
static void _drawRect(PdfGraphics c, double canvasH, double x, double y, double width, double height) { static void _drawRect(PdfGraphics c, double canvasH, double x, double y, double width, double height) {
c.drawRect(x, canvasH - (y + height), width, height); c.drawRect(x, canvasH - (y + height), width, height);
c.strokePath(); c.strokePath();
@@ -460,12 +415,7 @@ class PdfExportService {
c.strokePath(); c.strokePath();
} }
// ════════════════════════════════════════════════════════════════════════════ static List<List<String>> _buildTeamTableData(List<dynamic> players, Map<String, Map<String, dynamic>> statsMap) {
// TABELAS DE ESTATÍSTICAS
// ════════════════════════════════════════════════════════════════════════════
static List<List<String>> _buildTeamTableData(
List<dynamic> players, Map<String, Map<String, dynamic>> statsMap) {
List<List<String>> tableData = []; List<List<String>> tableData = [];
int tPts = 0, tFgm = 0, tFga = 0, tFtm = 0, tFta = 0, tFls = 0; int tPts = 0, tFgm = 0, tFga = 0, tFtm = 0, tFta = 0, tFls = 0;
@@ -485,27 +435,16 @@ class PdfExportService {
var s = statsMap[id] ?? {}; var s = statsMap[id] ?? {};
int pts = s['pts'] ?? 0; int pts = s['pts'] ?? 0;
int fgm = s['fgm'] ?? 0; int fgm = s['fgm'] ?? 0; int fga = s['fga'] ?? 0;
int fga = s['fga'] ?? 0; int ftm = s['ftm'] ?? 0; int fta = s['fta'] ?? 0;
int ftm = s['ftm'] ?? 0; int p2m = s['p2m'] ?? 0; int p2a = s['p2a'] ?? 0;
int fta = s['fta'] ?? 0; int p3m = s['p3m'] ?? 0; int p3a = s['p3a'] ?? 0;
int p2m = s['p2m'] ?? 0;
int p2a = s['p2a'] ?? 0;
int p3m = s['p3m'] ?? 0;
int p3a = s['p3a'] ?? 0;
int fls = s['fls'] ?? 0; int fls = s['fls'] ?? 0;
int orb = s['orb'] ?? 0; int orb = s['orb'] ?? 0; int drb = s['drb'] ?? 0;
int drb = s['drb'] ?? 0; int stl = s['stl'] ?? 0; int ast = s['ast'] ?? 0;
int stl = s['stl'] ?? 0; int tov = s['tov'] ?? 0; int blk = s['blk'] ?? 0;
int ast = s['ast'] ?? 0; int so = s['so'] ?? 0; int il = s['il'] ?? 0; int li = s['li'] ?? 0;
int tov = s['tov'] ?? 0; int pa = s['pa'] ?? 0; int tresS = s['tres_seg'] ?? 0; int dr = s['dr'] ?? 0;
int blk = s['blk'] ?? 0;
int so = s['so'] ?? 0;
int il = s['il'] ?? 0;
int li = s['li'] ?? 0;
int pa = s['pa'] ?? 0;
int tresS = s['tres_seg'] ?? 0;
int dr = s['dr'] ?? 0;
int sec = s['minutos_jogados'] ?? 0; int sec = s['minutos_jogados'] ?? 0;
tPts += pts; tFgm += fgm; tFga += fga; tFtm += ftm; tFta += fta; tPts += pts; tFgm += fgm; tFga += fga; tFtm += ftm; tFta += fta;
@@ -525,8 +464,7 @@ class PdfExportService {
tableData.add([ tableData.add([
p['number']?.toString() ?? '-', p['number']?.toString() ?? '-',
p['name']?.toString() ?? '?', p['name']?.toString() ?? '?',
minStr, minStr, pts.toString(),
pts.toString(),
p2m.toString(), p2a.toString(), p2Pct, p2m.toString(), p2a.toString(), p2Pct,
p3m.toString(), p3a.toString(), p3Pct, p3m.toString(), p3a.toString(), p3Pct,
fgm.toString(), fga.toString(), fgPct, fgm.toString(), fga.toString(), fgPct,
@@ -717,8 +655,7 @@ class PdfExportService {
); );
} }
static pw.Widget _groupHeader( static pw.Widget _groupHeader(String title, pw.TextStyle hStyle, pw.TextStyle sStyle) {
String title, pw.TextStyle hStyle, pw.TextStyle sStyle) {
return pw.Column( return pw.Column(
children: [ children: [
pw.Container( pw.Container(
@@ -726,54 +663,28 @@ class PdfExportService {
alignment: pw.Alignment.center, alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 2), padding: const pw.EdgeInsets.symmetric(vertical: 2),
decoration: const pw.BoxDecoration( decoration: const pw.BoxDecoration(
border: pw.Border( border: pw.Border(bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)),
bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)),
), ),
child: pw.Text(title, style: hStyle), child: pw.Text(title, style: hStyle),
), ),
pw.Row(children: [ pw.Row(children: [
pw.Expanded( pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('C', style: sStyle))),
child: pw.Container(
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 2),
child: pw.Text('C', style: sStyle))),
pw.Container(width: 0.5, height: 10, color: PdfColors.white), pw.Container(width: 0.5, height: 10, color: PdfColors.white),
pw.Expanded( pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('T', style: sStyle))),
child: pw.Container(
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 2),
child: pw.Text('T', style: sStyle))),
pw.Container(width: 0.5, height: 10, color: PdfColors.white), pw.Container(width: 0.5, height: 10, color: PdfColors.white),
pw.Expanded( pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('%', style: sStyle))),
child: pw.Container(
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 2),
child: pw.Text('%', style: sStyle))),
]), ]),
], ],
); );
} }
static pw.Widget _groupData( static pw.Widget _groupData(String c, String t, String pct, pw.TextStyle style) {
String c, String t, String pct, pw.TextStyle style) {
return pw.Row(children: [ return pw.Row(children: [
pw.Expanded( pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(c, style: style))),
child: pw.Container(
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 4),
child: pw.Text(c, style: style))),
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400), pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
pw.Expanded( pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(t, style: style))),
child: pw.Container(
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 4),
child: pw.Text(t, style: style))),
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400), pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
pw.Expanded( pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(pct, style: style))),
child: pw.Container(
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 4),
child: pw.Text(pct, style: style))),
]); ]);
} }
@@ -781,17 +692,8 @@ class PdfExportService {
return pw.Container( return pw.Container(
width: double.infinity, width: double.infinity,
padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: pw.BoxDecoration( decoration: pw.BoxDecoration(color: color, borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6))),
color: color, child: pw.Text(title, style: pw.TextStyle(color: PdfColors.white, fontSize: 14, fontWeight: pw.FontWeight.bold)),
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6)),
),
child: pw.Text(
title,
style: pw.TextStyle(
color: PdfColors.white,
fontSize: 14,
fontWeight: pw.FontWeight.bold),
),
); );
} }
@@ -799,15 +701,11 @@ class PdfExportService {
return pw.Row( return pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.center, mainAxisAlignment: pw.MainAxisAlignment.center,
children: [ children: [
pw.Container(width: 12, height: 12, pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.green600, shape: pw.BoxShape.circle)),
decoration: const pw.BoxDecoration(
color: PdfColors.green600, shape: pw.BoxShape.circle)),
pw.SizedBox(width: 4), pw.SizedBox(width: 4),
pw.Text('Cesto marcado', style: pw.TextStyle(fontSize: 10)), pw.Text('Cesto marcado', style: pw.TextStyle(fontSize: 10)),
pw.SizedBox(width: 20), pw.SizedBox(width: 20),
pw.Container(width: 12, height: 12, pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.red600, shape: pw.BoxShape.circle)),
decoration: const pw.BoxDecoration(
color: PdfColors.red600, shape: pw.BoxShape.circle)),
pw.SizedBox(width: 4), pw.SizedBox(width: 4),
pw.Text('Cesto falhado', style: pw.TextStyle(fontSize: 10)), pw.Text('Cesto falhado', style: pw.TextStyle(fontSize: 10)),
], ],
@@ -817,28 +715,15 @@ class PdfExportService {
static pw.Widget _buildSummaryBox(String title, String value) { static pw.Widget _buildSummaryBox(String title, String value) {
return pw.Container( return pw.Container(
width: 120, width: 120,
decoration: pw.BoxDecoration( decoration: pw.BoxDecoration(border: pw.TableBorder.all(color: PdfColors.black, width: 1)),
border: pw.TableBorder.all(color: PdfColors.black, width: 1),
),
child: pw.Column(children: [ child: pw.Column(children: [
pw.Container( pw.Container(
width: double.infinity, width: double.infinity, padding: const pw.EdgeInsets.all(6), color: const PdfColor.fromInt(0xFFA00000),
padding: const pw.EdgeInsets.all(6), child: pw.Text(title, style: pw.TextStyle(color: PdfColors.white, fontSize: 9, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center),
color: const PdfColor.fromInt(0xFFA00000),
child: pw.Text(title,
style: pw.TextStyle(
color: PdfColors.white,
fontSize: 9,
fontWeight: pw.FontWeight.bold),
textAlign: pw.TextAlign.center),
), ),
pw.Container( pw.Container(
width: double.infinity, width: double.infinity, padding: const pw.EdgeInsets.all(8),
padding: const pw.EdgeInsets.all(8), child: pw.Text(value, style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center),
child: pw.Text(value,
style: pw.TextStyle(
fontSize: 10, fontWeight: pw.FontWeight.bold),
textAlign: pw.TextAlign.center),
), ),
]), ]),
); );

View File

@@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
import 'package:playmaker/classe/theme.dart'; import 'package:playmaker/classe/theme.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:cached_network_image/cached_network_image.dart'; // 👇 IMPORTAÇÃO PARA CACHE import 'package:cached_network_image/cached_network_image.dart';
import 'package:shared_preferences/shared_preferences.dart'; // 👇 IMPORTAÇÃO PARA MEMÓRIA RÁPIDA import 'package:shared_preferences/shared_preferences.dart';
import '../utils/size_extension.dart'; import '../utils/size_extension.dart';
import 'login.dart'; import 'login.dart';
@@ -23,7 +23,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
File? _localImageFile; File? _localImageFile;
String? _uploadedImageUrl; String? _uploadedImageUrl;
bool _isUploadingImage = false; bool _isUploadingImage = false;
bool _isMemoryLoaded = false; // 👇 VARIÁVEL MÁGICA CONTRA O PISCAR bool _isMemoryLoaded = false;
final supabase = Supabase.instance.client; final supabase = Supabase.instance.client;
@@ -33,16 +33,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
_loadUserAvatar(); _loadUserAvatar();
} }
// 👇 LÊ A IMAGEM DA MEMÓRIA INSTANTANEAMENTE E CONFIRMA NA BD
Future<void> _loadUserAvatar() async { Future<void> _loadUserAvatar() async {
// 1. Lê da memória rápida primeiro!
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final savedUrl = prefs.getString('meu_avatar_guardado'); final savedUrl = prefs.getString('meu_avatar_guardado');
if (mounted) { if (mounted) {
setState(() { setState(() {
if (savedUrl != null) _uploadedImageUrl = savedUrl; if (savedUrl != null) _uploadedImageUrl = savedUrl;
_isMemoryLoaded = true; // Avisa que já leu a memória _isMemoryLoaded = true;
}); });
} }
@@ -59,7 +57,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (mounted && data != null && data['avatar_url'] != null) { if (mounted && data != null && data['avatar_url'] != null) {
final urlDoSupabase = data['avatar_url']; final urlDoSupabase = data['avatar_url'];
// Atualiza a memória se a foto na base de dados for diferente
if (urlDoSupabase != savedUrl) { if (urlDoSupabase != savedUrl) {
await prefs.setString('meu_avatar_guardado', urlDoSupabase); await prefs.setString('meu_avatar_guardado', urlDoSupabase);
setState(() { setState(() {
@@ -68,7 +65,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
} }
} }
} catch (e) { } catch (e) {
print("Erro ao carregar avatar: $e"); debugPrint("Erro ao carregar avatar: $e");
} }
} }
@@ -95,7 +92,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
fileOptions: const FileOptions(cacheControl: '3600', upsert: true) fileOptions: const FileOptions(cacheControl: '3600', upsert: true)
); );
final String publicUrl = supabase.storage.from('avatars').getPublicUrl(storagePath); // 👇 TRUQUE MÁGICO PARA O AVATAR ATUALIZAR: Adicionar o timestamp ao URL!
final String baseUrl = supabase.storage.from('avatars').getPublicUrl(storagePath);
final String publicUrl = '$baseUrl?v=${DateTime.now().millisecondsSinceEpoch}';
await supabase await supabase
.from('profiles') .from('profiles')
@@ -104,7 +103,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
'avatar_url': publicUrl 'avatar_url': publicUrl
}); });
// 👇 MÁGICA: GUARDA LOGO O NOVO URL NA MEMÓRIA PARA A HOME SABER!
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setString('meu_avatar_guardado', publicUrl); await prefs.setString('meu_avatar_guardado', publicUrl);
@@ -280,7 +278,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
} }
// 👇 AVATAR OTIMIZADO: SEM LAG, COM CACHE E MEMÓRIA
Widget _buildTappableProfileAvatar(BuildContext context, Color primaryRed) { Widget _buildTappableProfileAvatar(BuildContext context, Color primaryRed) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
@@ -298,29 +295,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
child: ClipOval( child: ClipOval(
child: _isUploadingImage && _localImageFile != null child: _isUploadingImage && _localImageFile != null
// 1. Mostrar imagem local (galeria) ENQUANTO está a fazer upload
? Image.file(_localImageFile!, fit: BoxFit.cover) ? Image.file(_localImageFile!, fit: BoxFit.cover)
// 2. Antes da memória carregar, fica só o fundo (evita piscar)
: !_isMemoryLoaded : !_isMemoryLoaded
? const SizedBox() ? const SizedBox()
// 3. Depois da memória carregar, se houver URL, desenha com Cache!
: _uploadedImageUrl != null && _uploadedImageUrl!.isNotEmpty : _uploadedImageUrl != null && _uploadedImageUrl!.isNotEmpty
? CachedNetworkImage( ? CachedNetworkImage(
imageUrl: _uploadedImageUrl!, imageUrl: _uploadedImageUrl!,
fit: BoxFit.cover, fit: BoxFit.cover,
fadeInDuration: Duration.zero, // Fica instantâneo! fadeInDuration: Duration.zero,
placeholder: (context, url) => const SizedBox(), placeholder: (context, url) => const SizedBox(),
errorWidget: (context, url, error) => Icon(Icons.person, color: primaryRed, size: 36 * context.sf), errorWidget: (context, url, error) => Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
) )
// 4. Se não houver URL, mete o boneco
: Icon(Icons.person, color: primaryRed, size: 36 * context.sf), : Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
), ),
), ),
// ÍCONE DE LÁPIS
Positioned( Positioned(
bottom: 0, bottom: 0,
right: 0, right: 0,
@@ -335,7 +324,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
), ),
// LOADING OVERLAY (Enquanto faz o upload)
if (_isUploadingImage) if (_isUploadingImage)
Positioned.fill( Positioned.fill(
child: Container( child: Container(
@@ -364,9 +352,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
onPressed: () async { onPressed: () async {
// Limpa a memória do Avatar ao sair para não aparecer na conta de outra pessoa! // 👇 AGORA LIMPA A EQUIPA E TUDO DA MEMÓRIA AO SAIR!
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.remove('meu_avatar_guardado'); await prefs.remove('meu_avatar_guardado');
await prefs.remove('last_team_id');
await prefs.remove('last_team_name');
await prefs.remove('last_team_logo');
await prefs.remove('last_team_wins');
await prefs.remove('last_team_losses');
await prefs.remove('last_team_draws');
await Supabase.instance.client.auth.signOut(); await Supabase.instance.client.auth.signOut();
if (ctx.mounted) { if (ctx.mounted) {

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:playmaker/classe/theme.dart'; import 'package:playmaker/classe/theme.dart';
import 'package:cached_network_image/cached_network_image.dart'; // 👇 A MAGIA DO CACHE import 'package:cached_network_image/cached_network_image.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../controllers/team_controller.dart'; import '../controllers/team_controller.dart';
import '../utils/size_extension.dart'; import '../utils/size_extension.dart';
@@ -18,9 +19,55 @@ class _StatusPageState extends State<StatusPage> {
String? _selectedTeamId; String? _selectedTeamId;
String _selectedTeamName = "Selecionar Equipa"; String _selectedTeamName = "Selecionar Equipa";
String? _selectedTeamLogo;
String _sortColumn = 'pts'; String _sortColumn = 'pts';
bool _isAscending = false; bool _isAscending = false;
@override
void initState() {
super.initState();
_loadSelectedTeam();
}
Future<void> _loadSelectedTeam() async {
final prefs = await SharedPreferences.getInstance();
final savedId = prefs.getString('last_team_id');
if (savedId != null && mounted) {
setState(() {
_selectedTeamId = savedId;
_selectedTeamName = prefs.getString('last_team_name') ?? "Selecionar Equipa";
_selectedTeamLogo = prefs.getString('last_team_logo');
});
}
}
Future<void> _saveSelectedTeam() async {
final prefs = await SharedPreferences.getInstance();
if (_selectedTeamId != null) {
await prefs.setString('last_team_id', _selectedTeamId!);
await prefs.setString('last_team_name', _selectedTeamName);
if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) {
await prefs.setString('last_team_logo', _selectedTeamLogo!);
} else {
await prefs.remove('last_team_logo');
}
}
final userId = _supabase.auth.currentUser?.id;
if (userId != null && _selectedTeamId != null) {
try {
await _supabase.from('profiles').upsert({
'id': userId,
'selected_team_id': _selectedTeamId,
});
} catch (e) {
debugPrint("Erro ao guardar equipa no Supabase: $e");
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bgColor = Theme.of(context).cardTheme.color ?? Colors.white; final bgColor = Theme.of(context).cardTheme.color ?? Colors.white;
@@ -44,7 +91,19 @@ class _StatusPageState extends State<StatusPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row(children: [ Row(children: [
Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf), (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty)
? ClipOval(
child: CachedNetworkImage(
imageUrl: _selectedTeamLogo!,
width: 24 * context.sf,
height: 24 * context.sf,
fit: BoxFit.cover,
placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
),
)
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
SizedBox(width: 10 * context.sf), SizedBox(width: 10 * context.sf),
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor)) Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
]), ]),
@@ -99,12 +158,11 @@ class _StatusPageState extends State<StatusPage> {
); );
} }
// 👇 AGORA GUARDA TAMBÉM O IMAGE_URL DO MEMBRO PARA MOSTRAR NA TABELA
List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) { List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
Map<String, Map<String, dynamic>> aggregated = {}; Map<String, Map<String, dynamic>> aggregated = {};
for (var member in members) { for (var member in members) {
String name = member['name']?.toString() ?? "Desconhecido"; String name = member['name']?.toString() ?? "Desconhecido";
String? imageUrl = member['image_url']?.toString(); // 👈 CAPTURA A IMAGEM AQUI String? imageUrl = member['image_url']?.toString();
aggregated[name] = {'name': name, 'image_url': imageUrl, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0}; aggregated[name] = {'name': name, 'image_url': imageUrl, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
} }
for (var row in stats) { for (var row in stats) {
@@ -140,78 +198,84 @@ class _StatusPageState extends State<StatusPage> {
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, Color bgColor, Color textColor) { Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, Color bgColor, Color textColor) {
return Container( return Container(
color: Colors.transparent, color: Colors.transparent, // 👇 VOLTOU A ESTAR TRANSPARENTE COMO TINHAS ANTES!
width: double.infinity,
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
physics: const BouncingScrollPhysics(),
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: DataTable( physics: const ClampingScrollPhysics(), // Mantém-se o Clamping para não puxar mais do que o ecrã
columnSpacing: 25 * context.sf, child: ConstrainedBox(
headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface), constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
dataRowMaxHeight: 60 * context.sf, child: DataTable(
dataRowMinHeight: 60 * context.sf, columnSpacing: 20 * context.sf,
columns: [ horizontalMargin: 16 * context.sf,
DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))), headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
_buildSortableColumn(context, 'J', 'j', textColor), dataRowMaxHeight: 60 * context.sf,
_buildSortableColumn(context, 'PTS', 'pts', textColor), dataRowMinHeight: 60 * context.sf,
_buildSortableColumn(context, 'AST', 'ast', textColor), columns: [
_buildSortableColumn(context, 'RBS', 'rbs', textColor), DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))),
_buildSortableColumn(context, 'STL', 'stl', textColor), _buildSortableColumn(context, 'J', 'j', textColor),
_buildSortableColumn(context, 'BLK', 'blk', textColor), _buildSortableColumn(context, 'PTS', 'pts', textColor),
_buildSortableColumn(context, 'DEF 🛡️', 'def', textColor), _buildSortableColumn(context, 'AST', 'ast', textColor),
_buildSortableColumn(context, 'MVP 🏆', 'mvp', textColor), _buildSortableColumn(context, 'RBS', 'rbs', textColor),
], _buildSortableColumn(context, 'STL', 'stl', textColor),
rows: [ _buildSortableColumn(context, 'BLK', 'blk', textColor),
...players.map((player) => DataRow(cells: [ _buildSortableColumn(context, 'DEF 🛡️', 'def', textColor),
DataCell( _buildSortableColumn(context, 'MVP 🏆', 'mvp', textColor),
Row( ],
children: [ rows: [
// 👇 FOTO DO JOGADOR NA TABELA (COM CACHE!) 👇 ...players.map((player) => DataRow(cells: [
ClipOval( DataCell(
child: Container( Row(
width: 30 * context.sf, children: [
height: 30 * context.sf, ClipOval(
color: Colors.grey.withOpacity(0.2), child: Container(
child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty) width: 30 * context.sf,
? CachedNetworkImage( height: 30 * context.sf,
imageUrl: player['image_url'], color: Colors.grey.withOpacity(0.2),
fit: BoxFit.cover, child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty)
fadeInDuration: Duration.zero, ? CachedNetworkImage(
placeholder: (context, url) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey), imageUrl: player['image_url'],
errorWidget: (context, url, error) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey), fit: BoxFit.cover,
) fadeInDuration: Duration.zero,
: Icon(Icons.person, size: 18 * context.sf, color: Colors.grey), placeholder: (context, url) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
errorWidget: (context, url, error) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
)
: Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
),
), ),
), SizedBox(width: 10 * context.sf),
SizedBox(width: 10 * context.sf), Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor))
Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor)) ]
] )
) ),
), DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))),
DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))), _buildStatCell(context, player['pts'], textColor, isHighlight: true),
_buildStatCell(context, player['pts'], textColor, isHighlight: true), _buildStatCell(context, player['ast'], textColor),
_buildStatCell(context, player['ast'], textColor), _buildStatCell(context, player['rbs'], textColor),
_buildStatCell(context, player['rbs'], textColor), _buildStatCell(context, player['stl'], textColor),
_buildStatCell(context, player['stl'], textColor), _buildStatCell(context, player['blk'], textColor),
_buildStatCell(context, player['blk'], textColor), _buildStatCell(context, player['def'], textColor, isBlue: true),
_buildStatCell(context, player['def'], textColor, isBlue: true), _buildStatCell(context, player['mvp'], textColor, isGold: true),
_buildStatCell(context, player['mvp'], textColor, isGold: true), ])),
])), DataRow(
DataRow( color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)),
color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)), cells: [
cells: [ DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: textColor, fontSize: 12 * context.sf))),
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: textColor, fontSize: 12 * context.sf))), DataCell(Center(child: Text(teamTotals['j'].toString(), style: TextStyle(fontWeight: FontWeight.bold, color: textColor)))),
DataCell(Center(child: Text(teamTotals['j'].toString(), style: TextStyle(fontWeight: FontWeight.bold, color: textColor)))), _buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true),
_buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true), _buildStatCell(context, teamTotals['ast'], textColor),
_buildStatCell(context, teamTotals['ast'], textColor), _buildStatCell(context, teamTotals['rbs'], textColor),
_buildStatCell(context, teamTotals['rbs'], textColor), _buildStatCell(context, teamTotals['stl'], textColor),
_buildStatCell(context, teamTotals['stl'], textColor), _buildStatCell(context, teamTotals['blk'], textColor),
_buildStatCell(context, teamTotals['blk'], textColor), _buildStatCell(context, teamTotals['def'], textColor, isBlue: true),
_buildStatCell(context, teamTotals['def'], textColor, isBlue: true), _buildStatCell(context, teamTotals['mvp'], textColor, isGold: true),
_buildStatCell(context, teamTotals['mvp'], textColor, isGold: true), ]
] )
) ],
], ),
), ),
), ),
), ),
@@ -247,10 +311,40 @@ class _StatusPageState extends State<StatusPage> {
stream: _teamController.teamsStream, stream: _teamController.teamsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
final teams = snapshot.data ?? []; final teams = snapshot.data ?? [];
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile( return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) {
title: Text(teams[i]['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), final team = teams[i];
onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); }, final logoUrl = team['image_url'];
));
return ListTile(
leading: ClipOval(
child: Container(
width: 36 * context.sf,
height: 36 * context.sf,
color: AppTheme.primaryRed.withOpacity(0.1),
child: (logoUrl != null && logoUrl.isNotEmpty)
? CachedNetworkImage(
imageUrl: logoUrl,
fit: BoxFit.cover,
placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
)
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
),
),
title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
onTap: () async {
setState(() {
_selectedTeamId = team['id'].toString();
_selectedTeamName = team['name'];
_selectedTeamLogo = logoUrl;
});
await _saveSelectedTeam();
if (context.mounted) Navigator.pop(context);
},
);
});
}, },
)); ));
} }

View File

@@ -42,10 +42,7 @@ class ActionSubtypeDialog extends StatelessWidget {
children: [ children: [
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(title, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
title,
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf),
),
), ),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
@@ -80,7 +77,7 @@ class ActionSubtypeDialog extends StatelessWidget {
side: BorderSide(color: Colors.white12, width: 1 * sf), side: BorderSide(color: Colors.white12, width: 1 * sf),
), ),
), ),
onPressed: () => onSelected(e.key), // Retorna a chave correta (ex: "tov_3s") onPressed: () => onSelected(e.key),
child: Text( child: Text(
e.value, e.value,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12 * sf), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12 * sf),
@@ -106,7 +103,6 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
final victimsColor = isCommitterOpponent ? AppTheme.myTeamBlue : AppTheme.oppTeamRed; final victimsColor = isCommitterOpponent ? AppTheme.myTeamBlue : AppTheme.oppTeamRed;
final possibleVictims = victimCourt.where((id) => !id.startsWith("fake_")).toList(); final possibleVictims = victimCourt.where((id) => !id.startsWith("fake_")).toList();
// Função interna para verificar se o jogador tem de sair
void checkFouledOut() { void checkFouledOut() {
final fouls = controller.playerStats[committerId]?["fls"] ?? 0; final fouls = controller.playerStats[committerId]?["fls"] ?? 0;
final isCourt = isCommitterOpponent ? controller.oppCourt.contains(committerId) : controller.myCourt.contains(committerId); final isCourt = isCommitterOpponent ? controller.oppCourt.contains(committerId) : controller.myCourt.contains(committerId);
@@ -116,12 +112,12 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
if (!context.mounted) return; if (!context.mounted) return;
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, // Obriga a fazer a substituição barrierDismissible: false,
builder: (ctx) => SubstitutionDialog( builder: (ctx) => SubstitutionDialog(
controller: controller, controller: controller,
isOpponent: isCommitterOpponent, isOpponent: isCommitterOpponent,
sf: sf, sf: sf,
forcedStarterId: committerId, // Passamos o jogador que foi expulso forcedStarterId: committerId,
), ),
); );
}); });
@@ -155,10 +151,7 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
children: [ children: [
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text("Quem sofreu a falta?", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
"Quem sofreu a falta?",
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf),
),
), ),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
@@ -190,7 +183,7 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
onTap: () { onTap: () {
Navigator.pop(ctx); Navigator.pop(ctx);
controller.registerFoul("$prefixCommitter$committerId", foulType, "$prefixVictim$id"); controller.registerFoul("$prefixCommitter$committerId", foulType, "$prefixVictim$id");
checkFouledOut(); // Verifica 5 faltas! checkFouledOut();
}, },
child: Container( child: Container(
width: 80 * sf, width: 80 * sf,
@@ -232,9 +225,9 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
icon: Icon(Icons.group, color: Colors.white, size: 16 * sf), icon: Icon(Icons.group, color: Colors.white, size: 16 * sf),
label: Text("Equipa / Sem Vítima Específica", style: TextStyle(fontSize: 12 * sf)), label: Text("Equipa / Sem Vítima Específica", style: TextStyle(fontSize: 12 * sf)),
onPressed: () { onPressed: () {
Navigator.pop(ctx); Navigator.pop(ctx);
controller.registerFoul("$prefixCommitter$committerId", foulType, ""); controller.registerFoul("$prefixCommitter$committerId", foulType, "");
checkFouledOut(); // Verifica 5 faltas! checkFouledOut();
}, },
) )
], ],
@@ -282,10 +275,7 @@ void showAssistDialog(BuildContext context, PlacarController controller, bool is
children: [ children: [
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text("Houve Assistência?", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
"Houve Assistência?",
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf),
),
), ),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
@@ -317,7 +307,11 @@ void showAssistDialog(BuildContext context, PlacarController controller, bool is
onTap: () { onTap: () {
Navigator.pop(ctx); Navigator.pop(ctx);
controller.commitStat("add_ast", "$prefix$id"); controller.commitStat("add_ast", "$prefix$id");
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Assistência: $name'), duration: const Duration(seconds: 1), backgroundColor: AppTheme.successGreen)); ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Assistência: $name'),
duration: const Duration(seconds: 1),
backgroundColor: AppTheme.successGreen,
));
}, },
child: Container( child: Container(
width: 75 * sf, width: 75 * sf,
@@ -383,135 +377,83 @@ class TopScoreboard extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.placarDarkSurface, color: AppTheme.placarDarkSurface,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(22 * sf), bottomLeft: Radius.circular(22 * sf),
bottomRight: Radius.circular(22 * sf)), bottomRight: Radius.circular(22 * sf),
),
border: Border.all(color: Colors.white, width: 2.0 * sf), border: Border.all(color: Colors.white, width: 2.0 * sf),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildTeamSection( _buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, AppTheme.myTeamBlue, false, sf),
controller.myTeam,
controller.myScore,
controller.myFouls,
controller.myTimeoutsUsed,
AppTheme.myTeamBlue,
false,
sf),
SizedBox(width: 20 * sf), SizedBox(width: 20 * sf),
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 4 * sf),
horizontal: 14 * sf, vertical: 4 * sf), decoration: BoxDecoration(color: AppTheme.placarTimerBg, borderRadius: BorderRadius.circular(9 * sf)),
decoration: BoxDecoration(
color: AppTheme.placarTimerBg,
borderRadius: BorderRadius.circular(9 * sf)),
child: ValueListenableBuilder<Duration>( child: ValueListenableBuilder<Duration>(
valueListenable: controller.durationNotifier, valueListenable: controller.durationNotifier,
builder: (context, duration, child) { builder: (context, duration, child) {
String formatTime = String formatTime = "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
"${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));
return Text(formatTime,
style: TextStyle(
color: Colors.white,
fontSize: 24 * sf,
fontWeight: FontWeight.w900,
fontFamily: 'monospace',
letterSpacing: 1.5 * sf));
}, },
), ),
), ),
SizedBox(height: 4 * sf), SizedBox(height: 4 * sf),
Text("PERÍODO ${controller.currentQuarter}", Text("PERÍODO ${controller.currentQuarter}", style: TextStyle(color: AppTheme.warningAmber, fontSize: 12 * sf, fontWeight: FontWeight.w900)),
style: TextStyle(
color: AppTheme.warningAmber,
fontSize: 12 * sf,
fontWeight: FontWeight.w900)),
], ],
), ),
SizedBox(width: 20 * sf), SizedBox(width: 20 * sf),
_buildTeamSection( _buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, AppTheme.oppTeamRed, true, sf),
controller.opponentTeam,
controller.opponentScore,
controller.opponentFouls,
controller.opponentTimeoutsUsed,
AppTheme.oppTeamRed,
true,
sf),
], ],
), ),
); );
} }
Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp, double sf) {
Color color, bool isOpp, double sf) {
int displayFouls = fouls > 5 ? 5 : fouls; int displayFouls = fouls > 5 ? 5 : fouls;
final timeoutIndicators = Row( final timeoutIndicators = Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: List.generate( children: List.generate(3, (index) => Container(
3, margin: EdgeInsets.symmetric(horizontal: 2.5 * sf),
(index) => Container( width: 10 * sf,
margin: EdgeInsets.symmetric(horizontal: 2.5 * sf), height: 10 * sf,
width: 10 * sf, decoration: BoxDecoration(
height: 10 * sf, shape: BoxShape.circle,
decoration: BoxDecoration( color: index < timeouts ? AppTheme.warningAmber : Colors.grey.shade600,
shape: BoxShape.circle, border: Border.all(color: Colors.white54, width: 1.0 * sf),
color: index < timeouts ),
? AppTheme.warningAmber )),
: Colors.grey.shade600,
border:
Border.all(color: Colors.white54, width: 1.0 * sf)),
)),
); );
List<Widget> content = [ List<Widget> content = [
Column(children: [ Column(children: [_scoreBox(score, color, sf), SizedBox(height: 5 * sf), timeoutIndicators]),
_scoreBox(score, color, sf),
SizedBox(height: 5 * sf),
timeoutIndicators
]),
SizedBox(width: 12 * sf), SizedBox(width: 12 * sf),
Column( Column(
crossAxisAlignment: crossAxisAlignment: isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end,
isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end,
children: [ children: [
Text(name.toUpperCase(), Text(name.toUpperCase(), style: TextStyle(color: Colors.white, fontSize: 16 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.0 * sf)),
style: TextStyle(
color: Colors.white,
fontSize: 16 * sf,
fontWeight: FontWeight.w900,
letterSpacing: 1.0 * sf)),
SizedBox(height: 3 * sf), SizedBox(height: 3 * sf),
Text("FALTAS: $displayFouls", Text("FALTAS: $displayFouls", style: TextStyle(color: displayFouls >= 5 ? AppTheme.actionMiss : AppTheme.warningAmber, fontSize: 11 * sf, fontWeight: FontWeight.bold)),
style: TextStyle(
color: displayFouls >= 5
? AppTheme.actionMiss
: AppTheme.warningAmber,
fontSize: 11 * sf,
fontWeight: FontWeight.bold)),
], ],
) )
]; ];
return Row( return Row(crossAxisAlignment: CrossAxisAlignment.center, children: isOpp ? content : content.reversed.toList());
crossAxisAlignment: CrossAxisAlignment.center,
children: isOpp ? content : content.reversed.toList());
} }
Widget _scoreBox(int score, Color color, double sf) => Container( Widget _scoreBox(int score, Color color, double sf) => Container(
width: 45 * sf, width: 45 * sf, height: 35 * sf,
height: 35 * sf, alignment: Alignment.center,
alignment: Alignment.center, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(6 * sf)),
decoration: BoxDecoration( child: Text(score.toString(), style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.w900)),
color: color, borderRadius: BorderRadius.circular(6 * sf)), );
child: Text(score.toString(),
style: TextStyle(
color: Colors.white,
fontSize: 20 * sf,
fontWeight: FontWeight.w900)),
);
} }
// ==============================================================================
// SHIRT PAINTER
// ==============================================================================
class ShirtPainter extends CustomPainter { class ShirtPainter extends CustomPainter {
final Color color; final Color color;
final bool isFouledOut; final bool isFouledOut;
@@ -523,15 +465,8 @@ class ShirtPainter extends CustomPainter {
final double h = size.height; final double h = size.height;
final Color shirtColor = isFouledOut ? Colors.grey.shade700 : color; final Color shirtColor = isFouledOut ? Colors.grey.shade700 : color;
final paint = Paint() final paint = Paint()..color = shirtColor..style = PaintingStyle.fill;
..color = shirtColor final trimPaint = Paint()..color = Colors.white..style = PaintingStyle.stroke..strokeWidth = w * 0.04..strokeJoin = StrokeJoin.round;
..style = PaintingStyle.fill;
final trimPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = w * 0.04
..strokeJoin = StrokeJoin.round;
final path = Path(); final path = Path();
path.moveTo(w * 0.32, h * 0.10); path.moveTo(w * 0.32, h * 0.10);
@@ -553,13 +488,61 @@ class ShirtPainter extends CustomPainter {
bool shouldRepaint(ShirtPainter old) => old.color != color || old.isFouledOut != isFouledOut; bool shouldRepaint(ShirtPainter old) => old.color != color || old.isFouledOut != isFouledOut;
} }
// ==============================================================================
// PLAYER COURT CARD — com feedback de camisola e troca de posições
// ==============================================================================
class PlayerCourtCard extends StatelessWidget { class PlayerCourtCard extends StatelessWidget {
final PlacarController controller; final PlacarController controller;
final String playerId; final String playerId;
final bool isOpponent; final bool isOpponent;
final double sf; final double sf;
const PlayerCourtCard({super.key, required this.controller, required this.playerId, required this.isOpponent, required this.sf}); const PlayerCourtCard({
super.key,
required this.controller,
required this.playerId,
required this.isOpponent,
required this.sf,
});
// ── Camisola flutuante mostrada durante o drag ─────────────────────────────
Widget _dragFeedback(String number, Color teamColor) {
const double size = 64;
return Material(
color: Colors.transparent,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: size,
height: size,
child: Stack(
alignment: Alignment.center,
children: [
CustomPaint(
size: const Size(size, size),
painter: ShirtPainter(color: teamColor),
),
Padding(
padding: const EdgeInsets.only(top: size * 0.15),
child: Text(
number,
style: TextStyle(
color: Colors.white,
fontSize: size * 0.38,
fontWeight: FontWeight.w900,
shadows: const [Shadow(color: Colors.black54, blurRadius: 3, offset: Offset(1, 1))],
),
),
),
],
),
),
],
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -571,42 +554,52 @@ class PlayerCourtCard extends StatelessWidget {
return Draggable<String>( return Draggable<String>(
data: "$prefix$playerId", data: "$prefix$playerId",
feedback: Material( // ✅ CORRIGIDO: mostra camisola + número durante o drag
color: Colors.transparent, feedback: _dragFeedback(number, teamColor),
child: Container( childWhenDragging: Opacity(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), opacity: 0.35,
decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(6)), child: _playerCardUI(number, realName, stats, teamColor, false, false, false, sf),
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>( child: DragTarget<String>(
onWillAcceptWithDetails: (details) {
final data = details.data;
// Jogadores da mesma equipa → troca de posição
if (data.startsWith("player_my_") || data.startsWith("player_opp_")) {
final sameTeam = isOpponent ? data.startsWith("player_opp_") : data.startsWith("player_my_");
return sameTeam && data != "$prefix$playerId";
}
return true; // aceita ações normais
},
onAcceptWithDetails: (details) { onAcceptWithDetails: (details) {
final action = details.data; final action = details.data;
if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") { // ── Troca de posição entre jogadores do campo ──────────────────
bool isMake = action.startsWith("add_"); if (action.startsWith("player_my_") || action.startsWith("player_opp_")) {
bool is3Pt = action.endsWith("_3"); final sameTeam = isOpponent ? action.startsWith("player_opp_") : action.startsWith("player_my_");
if (sameTeam && action != "$prefix$playerId") {
showDialog( controller.swapCourtPlayers(action, "$prefix$playerId");
context: context, }
builder: (ctx) => ZoneMapDialog( return;
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") { // ── Ações normais (inalteradas) ────────────────────────────────
if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") {
bool isMake = action.startsWith("add_");
bool is3Pt = action.endsWith("_3");
showDialog(
context: context,
builder: (ctx) => ZoneMapDialog(
playerName: realName,
isMake: isMake,
is3PointAction: is3Pt,
onZoneSelected: (zone, points, relX, relY) {
Navigator.pop(ctx);
controller.registerShotFromPopup(context, action, "$prefix$playerId", zone, points, relX, relY);
if (isMake) showAssistDialog(context, controller, isOpponent, playerId, sf);
},
),
);
} else if (action == "add_foul") {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => ActionSubtypeDialog( builder: (ctx) => ActionSubtypeDialog(
@@ -621,14 +614,11 @@ class PlayerCourtCard extends StatelessWidget {
sf: sf, sf: sf,
onSelected: (foulType) { onSelected: (foulType) {
Navigator.pop(ctx); 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); showFoulVictimDialog(context, controller, isOpponent, playerId, foulType, sf);
}, },
), ),
); );
} } else if (action == "add_tov") {
// ─── POP-UPS PARA TOV, STL E BLK ───
else if (action == "add_tov") {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => ActionSubtypeDialog( builder: (ctx) => ActionSubtypeDialog(
@@ -647,8 +637,7 @@ class PlayerCourtCard extends StatelessWidget {
}, },
), ),
); );
} } else if (action == "add_stl") {
else if (action == "add_stl") {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => ActionSubtypeDialog( builder: (ctx) => ActionSubtypeDialog(
@@ -664,8 +653,7 @@ class PlayerCourtCard extends StatelessWidget {
}, },
), ),
); );
} } else if (action == "add_blk") {
else if (action == "add_blk") {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => ActionSubtypeDialog( builder: (ctx) => ActionSubtypeDialog(
@@ -681,34 +669,55 @@ class PlayerCourtCard extends StatelessWidget {
}, },
), ),
); );
} } else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) {
// ─── FIM DOS POP-UPS ESPECIAIS ─── controller.handleActionDrag(context, action, "$prefix$playerId");
else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) { } else if (action.startsWith("bench_")) {
controller.handleActionDrag(context, action, "$prefix$playerId"); controller.handleSubbing(context, action, playerId, isOpponent);
}
else if (action.startsWith("bench_")) {
controller.handleSubbing(context, action, playerId, isOpponent);
} }
}, },
builder: (context, candidateData, rejectedData) { builder: (context, candidateData, rejectedData) {
bool isSubbing = candidateData.any((data) => data != null && (data.startsWith("bench_my_") || data.startsWith("bench_opp_"))); bool isSwapHover = candidateData.any((data) =>
bool isActionHover = candidateData.any((data) => data != null && (data.startsWith("add_") || data.startsWith("sub_") || data.startsWith("miss_"))); data != null &&
(data.startsWith("player_my_") || data.startsWith("player_opp_")) &&
data != "$prefix$playerId");
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); return _playerCardUI(number, realName, stats, teamColor, isSubbing, isActionHover, isSwapHover, sf);
}, },
), ),
); );
} }
Widget _playerCardUI(String number, String displayNameStr, Map<String, int> stats, Color teamColor, bool isSubbing, bool isActionHover, double sf) { Widget _playerCardUI(
String number,
String displayNameStr,
Map<String, int> stats,
Color teamColor,
bool isSubbing,
bool isActionHover,
bool isSwapHover,
double sf,
) {
bool isFouledOut = stats["fls"]! >= 5; bool isFouledOut = stats["fls"]! >= 5;
Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white; Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white;
Color borderColor = isFouledOut ? AppTheme.actionMiss : Colors.transparent; Color borderColor = isFouledOut ? AppTheme.actionMiss : Colors.transparent;
if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = AppTheme.myTeamBlue; } if (isSwapHover) {
else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = AppTheme.actionPoints; } bgColor = Colors.green.shade50;
borderColor = Colors.green.shade600;
} else 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"]!; int fgm = stats["fgm"]!;
int fga = stats["fga"]!;
String fgPercent = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) : "0"; String fgPercent = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) : "0";
String displayName = displayNameStr.length > 12 ? "${displayNameStr.substring(0, 10)}..." : displayNameStr; String displayName = displayNameStr.length > 12 ? "${displayNameStr.substring(0, 10)}..." : displayNameStr;
final double shirtSize = 40 * sf; final double shirtSize = 40 * sf;
@@ -734,10 +743,7 @@ class PlayerCourtCard extends StatelessWidget {
children: [ children: [
CustomPaint( CustomPaint(
size: Size(shirtSize, shirtSize), size: Size(shirtSize, shirtSize),
painter: ShirtPainter( painter: ShirtPainter(color: teamColor, isFouledOut: isFouledOut),
color: teamColor,
isFouledOut: isFouledOut,
),
), ),
Padding( Padding(
padding: EdgeInsets.only(top: shirtSize * 0.15), padding: EdgeInsets.only(top: shirtSize * 0.15),
@@ -761,10 +767,24 @@ class PlayerCourtCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(displayName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? AppTheme.actionMiss : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), 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), 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(
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)), "${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),
),
], ],
), ),
], ],
@@ -774,11 +794,15 @@ class PlayerCourtCard extends StatelessWidget {
} }
} }
// ==============================================================================
// SUBSTITUTION DIALOG
// ==============================================================================
class SubstitutionDialog extends StatefulWidget { class SubstitutionDialog extends StatefulWidget {
final PlacarController controller; final PlacarController controller;
final bool isOpponent; final bool isOpponent;
final double sf; final double sf;
final String? forcedStarterId; // <--- ADICIONADO PARA EXPULSÕES final String? forcedStarterId;
const SubstitutionDialog({ const SubstitutionDialog({
super.key, super.key,
@@ -804,26 +828,18 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
Color get teamColor => isOpp ? AppTheme.oppTeamRed : AppTheme.myTeamBlue; Color get teamColor => isOpp ? AppTheme.oppTeamRed : AppTheme.myTeamBlue;
String get teamName => isOpp ? ctrl.opponentTeam : ctrl.myTeam; String get teamName => isOpp ? ctrl.opponentTeam : ctrl.myTeam;
bool get canConfirm => _selectedStarterId != null && _selectedBenchId != null; bool get canConfirm => _selectedStarterId != null && _selectedBenchId != null;
bool get isForced => widget.forcedStarterId != null; // NOVO bool get isForced => widget.forcedStarterId != null;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Se for obrigado a sair, já aparece selecionado! if (isForced) _selectedStarterId = widget.forcedStarterId;
if (isForced) {
_selectedStarterId = widget.forcedStarterId;
}
} }
void _confirmSwap() { void _confirmSwap() {
if (!canConfirm) return; if (!canConfirm) return;
final benchPrefix = isOpp ? "bench_opp_" : "bench_my_"; final benchPrefix = isOpp ? "bench_opp_" : "bench_my_";
ctrl.handleSubbing( ctrl.handleSubbing(context, "$benchPrefix$_selectedBenchId", _selectedStarterId!, isOpp);
context,
"$benchPrefix$_selectedBenchId",
_selectedStarterId!,
isOpp,
);
Navigator.pop(context); Navigator.pop(context);
} }
@@ -838,7 +854,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1F2E), color: const Color(0xFF1A1F2E),
borderRadius: BorderRadius.circular(14 * sf), borderRadius: BorderRadius.circular(14 * sf),
border: Border.all(color: isForced ? AppTheme.actionMiss : const Color(0xFF2D3450), width: 2), // Borda vermelha se for expulsão border: Border.all(color: isForced ? AppTheme.actionMiss : const Color(0xFF2D3450), width: 2),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -846,7 +862,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 10 * sf), padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 10 * sf),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isForced ? AppTheme.actionMiss.withOpacity(0.8) : const Color(0xFF1E2540), // Fundo vermelho no título color: isForced ? AppTheme.actionMiss.withOpacity(0.8) : const Color(0xFF1E2540),
borderRadius: BorderRadius.vertical(top: Radius.circular(12 * sf)), borderRadius: BorderRadius.vertical(top: Radius.circular(12 * sf)),
border: const Border(bottom: BorderSide(color: Color(0xFF2D3450))), border: const Border(bottom: BorderSide(color: Color(0xFF2D3450))),
), ),
@@ -857,7 +873,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
isForced ? "SUBSTITUIÇÃO OBRIGATÓRIA (5 Faltas)" : "Substituição — ${teamName.toUpperCase()}", isForced ? "SUBSTITUIÇÃO OBRIGATÓRIA (5 Faltas)" : "Substituição — ${teamName.toUpperCase()}",
style: TextStyle(color: Colors.white, fontSize: 13 * sf, fontWeight: FontWeight.w600), style: TextStyle(color: Colors.white, fontSize: 13 * sf, fontWeight: FontWeight.w600),
), ),
if (!isForced) // Esconde o "X" de fechar se for forçado if (!isForced)
InkWell( InkWell(
onTap: () => Navigator.pop(context), onTap: () => Navigator.pop(context),
child: Container( child: Container(
@@ -876,10 +892,8 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
isStarter: true, isStarter: true,
activeColor: activeColor, activeColor: activeColor,
onTap: (id) { onTap: (id) {
if (isForced) return; // Se for forçado, não deixa clicar/desmarcar o titular if (isForced) return;
setState(() { setState(() => _selectedStarterId = _selectedStarterId == id ? null : id);
_selectedStarterId = _selectedStarterId == id ? null : id;
});
}, },
), ),
Divider(color: Colors.white12, height: 1, indent: 10 * sf, endIndent: 10 * sf), Divider(color: Colors.white12, height: 1, indent: 10 * sf, endIndent: 10 * sf),
@@ -893,13 +907,12 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
final fouls = ctrl.playerStats[id]?["fls"] ?? 0; final fouls = ctrl.playerStats[id]?["fls"] ?? 0;
if (fouls >= 5) { if (fouls >= 5) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('🛑 ${ctrl.playerNames[id]} expulso!'), content: Text('🛑 ${ctrl.playerNames[id]} expulso!'),
backgroundColor: AppTheme.actionMiss)); backgroundColor: AppTheme.actionMiss,
));
return; return;
} }
setState(() { setState(() => _selectedBenchId = _selectedBenchId == id ? null : id);
_selectedBenchId = _selectedBenchId == id ? null : id;
});
}, },
), ),
Padding( Padding(
@@ -914,7 +927,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
padding: EdgeInsets.fromLTRB(12 * sf, 0, 12 * sf, 12 * sf), padding: EdgeInsets.fromLTRB(12 * sf, 0, 12 * sf, 12 * sf),
child: Row( child: Row(
children: [ children: [
if (!isForced) // Esconde o botão de cancelar if (!isForced)
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.white12, backgroundColor: Colors.white12,
@@ -950,33 +963,24 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
} }
String _hintText() { String _hintText() {
if (isForced && _selectedBenchId == null) { if (isForced && _selectedBenchId == null) return "Um jogador atingiu as 5 faltas. Seleciona um suplente obrigatoriamente.";
return "Um jogador atingiu as 5 faltas. Seleciona um suplente obrigatoriamente."; if (_selectedStarterId == null && _selectedBenchId == null) return "Seleciona um titular e um suplente para fazer a troca";
} else if (_selectedStarterId == null && _selectedBenchId == null) { if (_selectedStarterId != null && _selectedBenchId == null) return "Agora seleciona o suplente que vai entrar";
return "Seleciona um titular e um suplente para fazer a troca"; if (_selectedStarterId == null && _selectedBenchId != null) return "Agora seleciona o titular que vai sair";
} else if (_selectedStarterId != null && _selectedBenchId == null) { final s = ctrl.playerNames[_selectedStarterId] ?? "";
return "Agora seleciona o suplente que vai entrar"; final sNum = ctrl.playerNumbers[_selectedStarterId] ?? "";
} else if (_selectedStarterId == null && _selectedBenchId != null) { final b = ctrl.playerNames[_selectedBenchId] ?? "";
return "Agora seleciona o titular que vai sair"; final bNum = ctrl.playerNumbers[_selectedBenchId] ?? "";
} else { return "#$sNum $s ↔ #$bNum $b";
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( Widget _sectionLabel(String label) => Padding(
padding: EdgeInsets.fromLTRB(12 * sf, 8 * sf, 12 * sf, 4 * sf), padding: EdgeInsets.fromLTRB(12 * sf, 8 * sf, 12 * sf, 4 * sf),
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(label.toUpperCase(), style: TextStyle(color: Colors.white38, fontSize: 10 * sf, letterSpacing: 0.8, fontWeight: FontWeight.w500)),
label.toUpperCase(), ),
style: TextStyle(color: Colors.white38, fontSize: 10 * sf, letterSpacing: 0.8, fontWeight: FontWeight.w500), );
),
),
);
Widget _playerGrid({ Widget _playerGrid({
required List<String> players, required List<String> players,
@@ -1021,10 +1025,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
CustomPaint( CustomPaint(size: Size(36 * sf, 36 * sf), painter: ShirtPainter(color: shirtColor, isFouledOut: isFouledOut)),
size: Size(36 * sf, 36 * sf),
painter: ShirtPainter(color: shirtColor, isFouledOut: isFouledOut),
),
Padding( Padding(
padding: EdgeInsets.only(top: 36 * sf * 0.15), padding: EdgeInsets.only(top: 36 * sf * 0.15),
child: Text( child: Text(
@@ -1042,12 +1043,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
), ),
), ),
SizedBox(height: 3 * sf), SizedBox(height: 3 * sf),
Text( Text(shortName, style: TextStyle(color: Colors.white70, fontSize: 9 * sf, fontWeight: FontWeight.w500), overflow: TextOverflow.ellipsis, textAlign: TextAlign.center),
shortName,
style: TextStyle(color: Colors.white70, fontSize: 9 * sf, fontWeight: FontWeight.w500),
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
if (isFouledOut) if (isFouledOut)
Container( Container(
margin: EdgeInsets.only(top: 2 * sf), margin: EdgeInsets.only(top: 2 * sf),
@@ -1068,6 +1064,11 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
); );
} }
} }
// ==============================================================================
// HEATMAP DIALOG
// ==============================================================================
class HeatmapDialog extends StatefulWidget { class HeatmapDialog extends StatefulWidget {
final List<dynamic> shots; final List<dynamic> shots;
final String myTeamName; final String myTeamName;
@@ -1166,18 +1167,11 @@ class _HeatmapDialogState extends State<HeatmapDialog> {
child: Column( child: Column(
children: [ children: [
InkWell( InkWell(
onTap: () => setState(() { onTap: () => setState(() { _selectedTeam = teamName; _selectedPlayerId = 'Todos'; _isMapVisible = true; }),
_selectedTeam = teamName;
_selectedPlayerId = 'Todos';
_isMapVisible = true;
}),
child: Container( child: Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(color: teamColor, borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8))),
color: teamColor,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)),
),
child: Column( child: Column(
children: [ children: [
Text(teamName.toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)), Text(teamName.toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)),
@@ -1205,11 +1199,7 @@ class _HeatmapDialogState extends State<HeatmapDialog> {
leading: Icon(Icons.person, color: teamColor), leading: Icon(Icons.person, color: teamColor),
title: Text(pName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)), title: Text(pName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)),
trailing: Text("$pts Pts", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: teamColor)), trailing: Text("$pts Pts", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: teamColor)),
onTap: () => setState(() { onTap: () => setState(() { _selectedTeam = teamName; _selectedPlayerId = pId; _isMapVisible = true; }),
_selectedTeam = teamName;
_selectedPlayerId = pId;
_isMapVisible = true;
}),
); );
}, },
), ),
@@ -1247,11 +1237,7 @@ class _HeatmapDialogState extends State<HeatmapDialog> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
child: Row(children: [ 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))]),
Icon(Icons.arrow_back, color: headerColor, size: 14),
const SizedBox(width: 4),
Text("VOLTAR", style: TextStyle(color: headerColor, fontWeight: FontWeight.bold, fontSize: 12)),
]),
), ),
), ),
), ),
@@ -1274,19 +1260,16 @@ class _HeatmapDialogState extends State<HeatmapDialog> {
child: LayoutBuilder(builder: (context, constraints) { child: LayoutBuilder(builder: (context, constraints) {
return Stack( return Stack(
children: [ children: [
CustomPaint( CustomPaint(size: Size(constraints.maxWidth, constraints.maxHeight), painter: HeatmapCourtPainter()),
size: Size(constraints.maxWidth, constraints.maxHeight),
painter: HeatmapCourtPainter(),
),
...filteredShots.map((shot) => Positioned( ...filteredShots.map((shot) => Positioned(
left: (shot.relativeX * constraints.maxWidth) - 8, left: (shot.relativeX * constraints.maxWidth) - 8,
top: (shot.relativeY * constraints.maxHeight) - 8, top: (shot.relativeY * constraints.maxHeight) - 8,
child: CircleAvatar( child: CircleAvatar(
radius: 8, radius: 8,
backgroundColor: shot.isMake ? AppTheme.successGreen : AppTheme.actionMiss, backgroundColor: shot.isMake ? AppTheme.successGreen : AppTheme.actionMiss,
child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white), child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white),
), ),
)), )),
], ],
); );
}), }),
@@ -1345,6 +1328,10 @@ class HeatmapCourtPainter extends CustomPainter {
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
} }
// ==============================================================================
// PLAY BY PLAY DIALOG
// ==============================================================================
class PlayByPlayDialog extends StatelessWidget { class PlayByPlayDialog extends StatelessWidget {
final PlacarController controller; final PlacarController controller;
const PlayByPlayDialog({super.key, required this.controller}); const PlayByPlayDialog({super.key, required this.controller});
@@ -1387,6 +1374,10 @@ class PlayByPlayDialog extends StatelessWidget {
} }
} }
// ==============================================================================
// BOX SCORE DIALOG
// ==============================================================================
class BoxScoreDialog extends StatelessWidget { class BoxScoreDialog extends StatelessWidget {
final PlacarController controller; final PlacarController controller;
final double sf; final double sf;
@@ -1400,10 +1391,7 @@ class BoxScoreDialog extends StatelessWidget {
builder: (context, child) { builder: (context, child) {
return Dialog( return Dialog(
backgroundColor: AppTheme.placarDarkSurface, backgroundColor: AppTheme.placarDarkSurface,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12 * sf), side: BorderSide(color: Colors.white24, width: 1 * sf)),
borderRadius: BorderRadius.circular(12 * sf),
side: BorderSide(color: Colors.white24, width: 1 * sf),
),
insetPadding: EdgeInsets.all(8 * sf), insetPadding: EdgeInsets.all(8 * sf),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: SizedBox( child: SizedBox(
@@ -1419,12 +1407,7 @@ class BoxScoreDialog extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text("BOX SCORE", style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)), Text("BOX SCORE", style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)),
IconButton( IconButton(icon: Icon(Icons.close, color: Colors.white, size: 24 * sf), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () => Navigator.pop(context))
icon: Icon(Icons.close, color: Colors.white, size: 24 * sf),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () => Navigator.pop(context),
)
], ],
), ),
), ),
@@ -1529,7 +1512,7 @@ class BoxScoreDialog extends StatelessWidget {
DataCell(Text((s['il'] ?? 0).toString(), style: const TextStyle(color: Colors.lightBlue))), 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['li'] ?? 0).toString(), style: const TextStyle(color: Colors.orangeAccent))),
DataCell(Text((s['pa'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), 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['tres_seg'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))),
DataCell(Text((s['dr'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), DataCell(Text((s['dr'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))),
DataCell(Text(fgText, style: const TextStyle(color: Colors.white54))), DataCell(Text(fgText, style: const TextStyle(color: Colors.white54))),
]); ]);

View File

@@ -9,6 +9,7 @@ import app_links
import file_selector_macos import file_selector_macos
import path_provider_foundation import path_provider_foundation
import printing import printing
import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
import url_launcher_macos import url_launcher_macos
@@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View File

@@ -45,10 +45,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.9" version: "3.6.1"
async: async:
dependency: transitive dependency: transitive
description: description:
@@ -121,6 +121,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@@ -177,6 +185,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.1" version: "0.3.1"
equatable:
dependency: transitive
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
excel:
dependency: "direct main"
description:
name: excel
sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780"
url: "https://pub.dev"
source: hosted
version: "4.0.6"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -189,10 +213,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.5" version: "2.2.0"
ffi_leak_tracker:
dependency: transitive
description:
name: ffi_leak_tracker
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
file: file:
dependency: transitive dependency: transitive
description: description:
@@ -288,6 +320,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.0" version: "2.5.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
gotrue: gotrue:
dependency: transitive dependency: transitive
description: description:
@@ -304,6 +344,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
hooks:
dependency: transitive
description:
name: hooks
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
http: http:
dependency: transitive dependency: transitive
description: description:
@@ -324,10 +372,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.0" version: "4.3.0"
image_cropper: image_cropper:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -496,6 +544,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.17.6"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@@ -624,14 +680,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
postgrest: postgrest:
dependency: transitive dependency: transitive
description: description:
@@ -656,6 +704,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.5+1" version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
qr: qr:
dependency: transitive dependency: transitive
description: description:
@@ -672,6 +728,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.7.0" version: "2.7.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
retry: retry:
dependency: transitive dependency: transitive
description: description:
@@ -688,6 +752,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.28.0" version: "0.28.0"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: a857d8b1479250aff6b57a51b2c02d31ca05848d441817c43f1640c885c286c0
url: "https://pub.dev"
source: hosted
version: "13.1.0"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: "7f7ae28cf400d13f811e297ff37742dba83b79e0a6f5dce14eec0248274e6ce9"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -997,6 +1077,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
win32:
dependency: transitive
description:
name: win32
sha256: ba7d5750e3441caa1bbe31d9e516348fcf8dfcb32aa29ef87a844a59f4d1f1d0
url: "https://pub.dev"
source: hosted
version: "6.1.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@@ -1013,6 +1101,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.6.1" version: "6.6.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
yet_another_json_isolate: yet_another_json_isolate:
dependency: transitive dependency: transitive
description: description:
@@ -1023,4 +1119,4 @@ packages:
version: "2.1.0" version: "2.1.0"
sdks: sdks:
dart: ">=3.10.0 <4.0.0" dart: ">=3.10.0 <4.0.0"
flutter: ">=3.38.0" flutter: ">=3.38.1"

View File

@@ -43,6 +43,8 @@ dependencies:
shared_preferences: ^2.5.4 shared_preferences: ^2.5.4
printing: ^5.14.3 printing: ^5.14.3
pdf: ^3.12.0 pdf: ^3.12.0
excel: ^4.0.6
share_plus: ^13.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -9,6 +9,7 @@
#include <app_links/app_links_plugin_c_api.h> #include <app_links/app_links_plugin_c_api.h>
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <printing/printing_plugin.h> #include <printing/printing_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
PrintingPluginRegisterWithRegistrar( PrintingPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PrintingPlugin")); registry->GetRegistrarForPlugin("PrintingPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
app_links app_links
file_selector_windows file_selector_windows
printing printing
share_plus
url_launcher_windows url_launcher_windows
) )