pdf e exel
This commit is contained in:
111
lib/controllers/active_team.dart
Normal file
111
lib/controllers/active_team.dart
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
375
lib/pages/excel_export_service.dart
Normal file
375
lib/pages/excel_export_service.dart
Normal 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 = ["Nº", "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 = ["Nº", "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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../models/game_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:playmaker/pages/PlacarPage.dart';
|
||||
import 'package:playmaker/classe/theme.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import '../controllers/team_controller.dart';
|
||||
import '../controllers/game_controller.dart';
|
||||
import '../models/game_model.dart';
|
||||
import '../utils/size_extension.dart';
|
||||
|
||||
import 'pdf_export_service.dart';
|
||||
import 'excel_export_service.dart';
|
||||
|
||||
class GameResultCard extends StatelessWidget {
|
||||
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,
|
||||
});
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
top: -10 * sf,
|
||||
right: -10 * sf,
|
||||
top: -12 * sf,
|
||||
right: -12 * sf,
|
||||
child: PopupMenuButton<String>(
|
||||
icon: Icon(Icons.more_vert, color: Colors.grey.shade600, size: 26 * sf), // Ícone um pouco maior
|
||||
splashRadius: 24 * sf,
|
||||
elevation: 8, // Adiciona sombra para não se misturar com o fundo
|
||||
shadowColor: Colors.black45,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16 * sf)),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
surfaceTintColor: Theme.of(context).colorScheme.surface, // Previne que o material 3 mude a cor
|
||||
onSelected: (value) {
|
||||
if (value == 'pdf' || value == 'excel') {
|
||||
_showTeamSelectionDialog(context, value);
|
||||
} else if (value == 'delete') {
|
||||
_showDeleteConfirmation(context);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'pdf',
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.picture_as_pdf, color: AppTheme.primaryRed.withOpacity(0.8), size: 22 * sf),
|
||||
splashRadius: 20 * sf,
|
||||
tooltip: 'Gerar PDF',
|
||||
onPressed: () async {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('A gerar PDF...'), duration: Duration(seconds: 1)));
|
||||
await PdfExportService.generateAndPrintBoxScore(
|
||||
gameId: gameId,
|
||||
myTeam: myTeam,
|
||||
opponentTeam: opponentTeam,
|
||||
myScore: myScore,
|
||||
opponentScore: opponentScore,
|
||||
season: season,
|
||||
);
|
||||
},
|
||||
// Í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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'excel',
|
||||
child: Row(
|
||||
children: [
|
||||
// Í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)),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_outline, color: Colors.grey.shade400, size: 22 * sf),
|
||||
splashRadius: 20 * sf,
|
||||
tooltip: 'Eliminar Jogo',
|
||||
onPressed: () => _showDeleteConfirmation(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -23,6 +23,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
final TeamController _teamController = TeamController();
|
||||
String? _selectedTeamId;
|
||||
String _selectedTeamName = "Selecionar Equipa";
|
||||
String? _selectedTeamLogo;
|
||||
|
||||
int _teamWins = 0;
|
||||
int _teamLosses = 0;
|
||||
@@ -31,47 +32,113 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
final _supabase = Supabase.instance.client;
|
||||
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
_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 {
|
||||
// 1. LÊ DA MEMÓRIA RÁPIDA PRIMEIRO
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedUrl = prefs.getString('meu_avatar_guardado');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
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;
|
||||
if (userId == null) return;
|
||||
|
||||
try {
|
||||
final data = await _supabase
|
||||
.from('profiles')
|
||||
.select('avatar_url')
|
||||
.eq('id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
final data = await _supabase.from('profiles').select('avatar_url').eq('id', userId).maybeSingle();
|
||||
if (mounted && data != null && data['avatar_url'] != null) {
|
||||
final urlDoSupabase = data['avatar_url'];
|
||||
|
||||
// Se a foto na base de dados for nova, ele guarda e atualiza!
|
||||
if (urlDoSupabase != savedUrl) {
|
||||
await prefs.setString('meu_avatar_guardado', urlDoSupabase);
|
||||
setState(() {
|
||||
_avatarUrl = urlDoSupabase;
|
||||
});
|
||||
setState(() { _avatarUrl = urlDoSupabase; });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -85,7 +152,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
_buildHomeContent(context),
|
||||
const GamePage(),
|
||||
const TeamsPage(),
|
||||
const StatusPage(),
|
||||
StatusPage(key: ValueKey(_statusKey)), // A StatusPage recarrega sempre que a chave muda!
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
@@ -95,55 +162,37 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
backgroundColor: AppTheme.primaryRed,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
|
||||
leading: Padding(
|
||||
padding: EdgeInsets.all(10.0 * context.sf),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
||||
);
|
||||
await Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen()));
|
||||
_loadUserAvatar();
|
||||
},
|
||||
// SÓ MOSTRA A IMAGEM OU O BONECO DEPOIS DE LER A MEMÓRIA
|
||||
child: !_isMemoryLoaded
|
||||
// Nos primeiros 0.05 segs, mostra só o círculo de fundo (sem boneco)
|
||||
? CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2))
|
||||
|
||||
// Depois da memória responder:
|
||||
: _avatarUrl != null && _avatarUrl!.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl: _avatarUrl!,
|
||||
fadeInDuration: Duration.zero, // Corta o atraso visual!
|
||||
imageBuilder: (context, imageProvider) => CircleAvatar(
|
||||
backgroundColor: Colors.white.withOpacity(0.2),
|
||||
backgroundImage: imageProvider,
|
||||
),
|
||||
fadeInDuration: Duration.zero,
|
||||
imageBuilder: (context, imageProvider) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), backgroundImage: imageProvider),
|
||||
placeholder: (context, url) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)),
|
||||
errorWidget: (context, url, error) => CircleAvatar(
|
||||
backgroundColor: Colors.white.withOpacity(0.2),
|
||||
child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf),
|
||||
),
|
||||
errorWidget: (context, url, error) => CircleAvatar(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(
|
||||
selectedIndex: _selectedIndex,
|
||||
onDestinationSelected: (index) => setState(() => _selectedIndex = index),
|
||||
onDestinationSelected: (index) {
|
||||
setState(() => _selectedIndex = index);
|
||||
if (index == 0) {
|
||||
_loadSelectedTeam();
|
||||
}
|
||||
},
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
|
||||
elevation: 1,
|
||||
@@ -167,13 +216,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||
stream: _teamController.teamsStream,
|
||||
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.data!.isEmpty) {
|
||||
return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface))));
|
||||
}
|
||||
if (!snapshot.hasData && snapshot.connectionState == ConnectionState.waiting) 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!;
|
||||
return ListView.builder(
|
||||
@@ -181,18 +225,33 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
itemCount: teams.length,
|
||||
itemBuilder: (context, index) {
|
||||
final team = teams[index];
|
||||
final String? logoUrl = team['image_url'];
|
||||
|
||||
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)),
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
_selectedTeamId = team['id'].toString();
|
||||
_selectedTeamName = team['name'] ?? 'Desconhecido';
|
||||
_selectedTeamLogo = logoUrl;
|
||||
_teamWins = int.tryParse(team['wins']?.toString() ?? '0') ?? 0;
|
||||
_teamLosses = int.tryParse(team['losses']?.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),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(12 * context.sf),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
borderRadius: BorderRadius.circular(15 * context.sf),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2))
|
||||
),
|
||||
decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
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),
|
||||
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
|
||||
]),
|
||||
@@ -263,17 +320,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
children: [
|
||||
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),
|
||||
Expanded(
|
||||
child: PieChartCard(
|
||||
victories: _teamWins,
|
||||
defeats: _teamLosses,
|
||||
draws: _teamDraws,
|
||||
title: 'DESEMPENHO',
|
||||
subtitle: 'Temporada',
|
||||
backgroundColor: AppTheme.statPieBg,
|
||||
sf: context.sf
|
||||
),
|
||||
),
|
||||
Expanded(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"
|
||||
? Container(
|
||||
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))],
|
||||
),
|
||||
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))]),
|
||||
child: Column(
|
||||
children: [
|
||||
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),
|
||||
),
|
||||
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)),
|
||||
SizedBox(height: 20 * context.sf),
|
||||
Text("Nenhuma Equipa Ativa", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: textColor)),
|
||||
SizedBox(height: 8 * context.sf),
|
||||
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),
|
||||
),
|
||||
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)),
|
||||
SizedBox(height: 24 * context.sf),
|
||||
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)),
|
||||
),
|
||||
),
|
||||
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)))),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -330,11 +348,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
stream: _supabase.from('games').stream(primaryKey: ['id']).order('game_date', ascending: false),
|
||||
builder: (context, gameSnapshot) {
|
||||
if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
|
||||
|
||||
// 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());
|
||||
}
|
||||
if (!gameSnapshot.hasData && gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
|
||||
|
||||
final todosOsJogos = gameSnapshot.data ?? [];
|
||||
final gamesList = todosOsJogos.where((game) {
|
||||
@@ -344,44 +358,19 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado';
|
||||
}).take(3).toList();
|
||||
|
||||
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)),
|
||||
);
|
||||
}
|
||||
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 Column(
|
||||
children: gamesList.map((game) {
|
||||
String dbMyTeam = game['my_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;
|
||||
|
||||
String dbMyTeam = game['my_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;
|
||||
String opponent; int myScore; int oppScore;
|
||||
|
||||
if (dbMyTeam == _selectedTeamName) {
|
||||
opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore;
|
||||
} else {
|
||||
opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore;
|
||||
}
|
||||
if (dbMyTeam == _selectedTeamName) { opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore; } else { opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore; }
|
||||
String rawDate = game['game_date']?.toString() ?? '---'; String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
|
||||
String result = myScore > oppScore ? 'V' : (myScore < oppScore ? 'D' : 'E');
|
||||
|
||||
String rawDate = game['game_date']?.toString() ?? '---';
|
||||
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'] ?? '---',
|
||||
);
|
||||
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(),
|
||||
);
|
||||
},
|
||||
@@ -404,39 +393,20 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
astMap[pid] = (astMap[pid] ?? 0) + (int.tryParse(row['ast']?.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};
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0};
|
||||
String getBest(Map<String, int> map) { if (map.isEmpty) return '---'; return namesMap[map.entries.reduce((a, b) => a.value > b.value ? a : b).key] ?? '---'; }
|
||||
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}) {
|
||||
return Card(
|
||||
elevation: 4, margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: AppTheme.warningAmber, width: 2) : BorderSide.none),
|
||||
elevation: 4, margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: AppTheme.warningAmber, width: 2) : BorderSide.none),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final double ch = constraints.maxHeight;
|
||||
final double cw = constraints.maxWidth;
|
||||
final double ch = constraints.maxHeight; final double cw = constraints.maxWidth;
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(cw * 0.06),
|
||||
child: Column(
|
||||
@@ -444,23 +414,13 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
children: [
|
||||
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(
|
||||
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)),
|
||||
),
|
||||
),
|
||||
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)))),
|
||||
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)))),
|
||||
SizedBox(height: ch * 0.015),
|
||||
Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))),
|
||||
const Spacer(),
|
||||
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)))
|
||||
),
|
||||
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)))),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -470,33 +430,20 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
}) {
|
||||
bool isWin = result == 'V';
|
||||
bool isDraw = result == 'E';
|
||||
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}) {
|
||||
bool isWin = result == 'V'; bool isDraw = result == 'E';
|
||||
Color statusColor = isWin ? AppTheme.successGreen : (isDraw ? AppTheme.warningAmber : AppTheme.oppTeamRed);
|
||||
final bgColor = Theme.of(context).cardTheme.color;
|
||||
final textColor = Theme.of(context).colorScheme.onSurface;
|
||||
final bgColor = Theme.of(context).cardTheme.color; final textColor = Theme.of(context).colorScheme.onSurface;
|
||||
|
||||
return Container(
|
||||
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))],
|
||||
),
|
||||
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))]),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(14 * context.sf),
|
||||
child: Row(
|
||||
children: [
|
||||
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))),
|
||||
),
|
||||
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)))),
|
||||
SizedBox(width: 14 * context.sf),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -508,14 +455,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
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)),
|
||||
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(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)),
|
||||
],
|
||||
),
|
||||
@@ -527,30 +467,14 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.withOpacity(0.1), thickness: 1.5),
|
||||
Container(
|
||||
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))),
|
||||
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))),
|
||||
child: Column(
|
||||
children: [
|
||||
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)),
|
||||
],
|
||||
),
|
||||
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))]),
|
||||
SizedBox(height: 8 * context.sf),
|
||||
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)),
|
||||
],
|
||||
),
|
||||
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))]),
|
||||
SizedBox(height: 8 * context.sf),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)),
|
||||
const Expanded(child: SizedBox()),
|
||||
],
|
||||
),
|
||||
Row(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}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 14 * context.sf, color: color),
|
||||
SizedBox(width: 4 * context.sf),
|
||||
Icon(icon, size: 14 * context.sf, color: color), SizedBox(width: 4 * context.sf),
|
||||
Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)),
|
||||
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
|
||||
)
|
||||
),
|
||||
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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:printing/printing.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
// Modelo local para os tiros
|
||||
class _ShotDot {
|
||||
final double relX;
|
||||
final double relY;
|
||||
@@ -13,10 +12,6 @@ class _ShotDot {
|
||||
}
|
||||
|
||||
class PdfExportService {
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// ENTRY POINT
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
static Future<void> generateAndPrintBoxScore({
|
||||
required String gameId,
|
||||
required String myTeam,
|
||||
@@ -24,18 +19,15 @@ class PdfExportService {
|
||||
required String myScore,
|
||||
required String opponentScore,
|
||||
required String season,
|
||||
required String targetTeam,
|
||||
}) async {
|
||||
final supabase = Supabase.instance.client;
|
||||
|
||||
// ── Jogo ────────────────────────────────────────────────────────────────
|
||||
final gameData =
|
||||
await supabase.from('games').select().eq('id', gameId).single();
|
||||
final gameData = await supabase.from('games').select().eq('id', gameId).single();
|
||||
|
||||
// ── Equipas ─────────────────────────────────────────────────────────────
|
||||
final teamsData = await supabase
|
||||
.from('teams')
|
||||
.select('id, name')
|
||||
.inFilter('name', [myTeam, opponentTeam]);
|
||||
final teamsData = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
|
||||
|
||||
String? myTeamId;
|
||||
for (var t in teamsData) {
|
||||
@@ -44,32 +36,19 @@ class PdfExportService {
|
||||
|
||||
// ── Jogadores (Apenas a minha equipa) ───────────────────────────────────
|
||||
List<dynamic> myPlayers = myTeamId != null
|
||||
? await supabase
|
||||
.from('members')
|
||||
.select()
|
||||
.eq('team_id', myTeamId)
|
||||
.eq('type', 'Jogador')
|
||||
? await supabase.from('members').select().eq('team_id', myTeamId).eq('type', 'Jogador')
|
||||
: [];
|
||||
|
||||
// ── Estatísticas ─────────────────────────────────────────────────────────
|
||||
final statsData =
|
||||
await supabase.from('player_stats').select().eq('game_id', gameId);
|
||||
final statsData = await supabase.from('player_stats').select().eq('game_id', gameId);
|
||||
Map<String, Map<String, dynamic>> statsMap = {};
|
||||
for (var s in statsData) {
|
||||
statsMap[s['member_id'].toString()] = s;
|
||||
}
|
||||
|
||||
// ── Tiros (para o mapa de calor da minha equipa) ──────────────────────
|
||||
final shotsData = await supabase
|
||||
.from('shot_locations')
|
||||
.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
|
||||
// ── Tiros ──────────────────────
|
||||
final shotsData = await supabase.from('shot_locations').select().eq('game_id', gameId);
|
||||
final Set<String> myPlayerIds = myPlayers.map((p) => p['id'].toString()).toSet();
|
||||
final List<_ShotDot> myTeamShots = [];
|
||||
final Map<String, List<_ShotDot>> shotsByPlayer = {};
|
||||
|
||||
@@ -86,16 +65,14 @@ class PdfExportService {
|
||||
shotsByPlayer.putIfAbsent(memberId, () => []).add(dot);
|
||||
}
|
||||
|
||||
// ── Tabela de estatísticas (Apenas a minha equipa) ────────────────────
|
||||
List<List<String>> myTeamTable =
|
||||
_buildTeamTableData(myPlayers, statsMap);
|
||||
// ── Tabela de estatísticas ────────────────────
|
||||
List<List<String>> myTeamTable = _buildTeamTableData(myPlayers, statsMap);
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// CONSTRUÇÃO DO PDF
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
final pdf = pw.Document();
|
||||
|
||||
// ── PÁGINA 1: Box Score ──────────────────────────────────────────────
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
pageFormat: PdfPageFormat.a4.landscape,
|
||||
@@ -110,71 +87,81 @@ class PdfExportService {
|
||||
children: [
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text('Relatório Estatístico',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: pw.FontWeight.bold)),
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
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(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||
children: [
|
||||
pw.Text('$myTeam vs $opponentTeam',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text('Resultado: $myScore — $opponentScore',
|
||||
style: const pw.TextStyle(fontSize: 13)),
|
||||
pw.Text('Época: $season',
|
||||
style: const pw.TextStyle(fontSize: 11)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 12),
|
||||
pw.Text('$myTeam vs $opponentTeam', style: pw.TextStyle(fontSize: 15, fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text('Resultado: $myScore — $opponentScore', style: const pw.TextStyle(fontSize: 13)),
|
||||
pw.Text('Época: $season', style: const pw.TextStyle(fontSize: 11)),
|
||||
pw.SizedBox(height: 10),
|
||||
|
||||
pw.Text('Equipa: $myTeam',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: const PdfColor.fromInt(0xFFA00000))),
|
||||
// 👇 NOVA TABELA: PONTUAÇÃO POR PERÍODO 👇
|
||||
pw.Table.fromTextArray(
|
||||
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: 8),
|
||||
|
||||
pw.Text('Pontos e Lançamentos',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColors.grey700)),
|
||||
pw.Text('Pontos e Lançamentos', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
|
||||
pw.SizedBox(height: 2),
|
||||
_buildPdfTablePart1(
|
||||
myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
||||
_buildPdfTablePart1(myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
||||
|
||||
pw.SizedBox(height: 14),
|
||||
|
||||
pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColors.grey700)),
|
||||
pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
|
||||
pw.SizedBox(height: 2),
|
||||
_buildPdfTablePart2(
|
||||
myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
||||
_buildPdfTablePart2(myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
||||
|
||||
pw.SizedBox(height: 16),
|
||||
|
||||
pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSummaryBox('Melhor Marcador',
|
||||
gameData['top_pts_name'] ?? '---'),
|
||||
_buildSummaryBox('Melhor Marcador', gameData['top_pts_name'] ?? '---'),
|
||||
pw.SizedBox(width: 10),
|
||||
_buildSummaryBox('Melhor Ressaltador',
|
||||
gameData['top_rbs_name'] ?? '---'),
|
||||
_buildSummaryBox('Melhor Ressaltador', gameData['top_rbs_name'] ?? '---'),
|
||||
pw.SizedBox(width: 10),
|
||||
_buildSummaryBox('Melhor Passador',
|
||||
gameData['top_ast_name'] ?? '---'),
|
||||
_buildSummaryBox('Melhor Passador', gameData['top_ast_name'] ?? '---'),
|
||||
pw.SizedBox(width: 10),
|
||||
_buildSummaryBox(
|
||||
'MVP', gameData['mvp_name'] ?? '---'),
|
||||
_buildSummaryBox('MVP', gameData['mvp_name'] ?? '---'),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -195,15 +182,13 @@ class PdfExportService {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
_heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)',
|
||||
const PdfColor.fromInt(0xFFA00000)),
|
||||
_heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)', const PdfColor.fromInt(0xFFA00000)),
|
||||
pw.SizedBox(height: 12),
|
||||
pw.Expanded(
|
||||
child: pw.Center(
|
||||
child: pw.CustomPaint(
|
||||
size: const PdfPoint(360, 360),
|
||||
painter: (canvas, size) =>
|
||||
_paintCourt(canvas, size, myTeamShots),
|
||||
painter: (canvas, size) => _paintCourt(canvas, size, myTeamShots),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -217,7 +202,6 @@ class PdfExportService {
|
||||
}
|
||||
|
||||
// ── 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 pid = p['id'].toString();
|
||||
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) {
|
||||
final String playerName = player['name']?.toString() ?? 'Jogador';
|
||||
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) {
|
||||
final double w = size.x;
|
||||
final double h = size.y;
|
||||
final double basketX = w / 2;
|
||||
|
||||
// Fundo Amarelo (Toda a área)
|
||||
canvas
|
||||
..setFillColor(const PdfColor.fromInt(0xFFDFAB00))
|
||||
..drawRect(0, 0, w, h)
|
||||
@@ -341,7 +317,6 @@ class PdfExportService {
|
||||
final double alturaDoArco = larguraDoArco * 0.30;
|
||||
final double totalArcoHeight = alturaDoArco * 4;
|
||||
|
||||
// ── 1. LINHAS BRANCAS ───────────────────────────────────────────────
|
||||
canvas.setStrokeColor(PdfColors.white);
|
||||
canvas.setLineWidth(2.0);
|
||||
|
||||
@@ -350,7 +325,6 @@ class PdfExportService {
|
||||
_drawLine(canvas, h, 0, length, margin, length);
|
||||
_drawLine(canvas, h, w - margin, length, w, length);
|
||||
|
||||
// Arco 3pts
|
||||
_drawEllipseArc(canvas, h, basketX, length, larguraDoArco, totalArcoHeight / 2, 0, math.pi);
|
||||
|
||||
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, sXR, sYR, w, h * 0.85);
|
||||
|
||||
// ── 2. LINHAS PRETAS ─────────────────────────────────────────────────
|
||||
canvas.setStrokeColor(PdfColors.black);
|
||||
canvas.setLineWidth(1.5);
|
||||
|
||||
final double pW = w * 0.28;
|
||||
final double pH = h * 0.38;
|
||||
|
||||
// Garrafão
|
||||
_drawRect(canvas, h, basketX - pW / 2, 0, pW, pH);
|
||||
|
||||
// Círculo Lances Livres
|
||||
final double ftR = pW / 2;
|
||||
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, 0, math.pi);
|
||||
// Tracejado
|
||||
for (int i = 0; i < 10; i++) {
|
||||
_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, sXR, sYR);
|
||||
|
||||
// Meio Campo
|
||||
_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);
|
||||
_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) {
|
||||
final double px = shot.relX * w;
|
||||
final double py = shot.relY * h;
|
||||
|
||||
final PdfColor dotColor = shot.isMake ? PdfColors.green600 : PdfColors.red600;
|
||||
|
||||
// Desenha Círculo Colorido
|
||||
_fillCircle(canvas, h, px, py, 6, dotColor);
|
||||
|
||||
// Símbolos
|
||||
canvas.setStrokeColor(PdfColors.white);
|
||||
canvas.setLineWidth(1.2);
|
||||
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) {
|
||||
c.moveTo(x1, canvasH - y1);
|
||||
c.lineTo(x2, canvasH - y2);
|
||||
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) {
|
||||
c.drawRect(x, canvasH - (y + height), width, height);
|
||||
c.strokePath();
|
||||
@@ -460,12 +415,7 @@ class PdfExportService {
|
||||
c.strokePath();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// TABELAS DE ESTATÍSTICAS
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
static List<List<String>> _buildTeamTableData(
|
||||
List<dynamic> players, Map<String, Map<String, dynamic>> statsMap) {
|
||||
static List<List<String>> _buildTeamTableData(List<dynamic> players, Map<String, Map<String, dynamic>> statsMap) {
|
||||
List<List<String>> tableData = [];
|
||||
|
||||
int tPts = 0, tFgm = 0, tFga = 0, tFtm = 0, tFta = 0, tFls = 0;
|
||||
@@ -485,27 +435,16 @@ class PdfExportService {
|
||||
var s = statsMap[id] ?? {};
|
||||
|
||||
int pts = s['pts'] ?? 0;
|
||||
int fgm = s['fgm'] ?? 0;
|
||||
int fga = s['fga'] ?? 0;
|
||||
int ftm = s['ftm'] ?? 0;
|
||||
int fta = s['fta'] ?? 0;
|
||||
int p2m = s['p2m'] ?? 0;
|
||||
int p2a = s['p2a'] ?? 0;
|
||||
int p3m = s['p3m'] ?? 0;
|
||||
int p3a = s['p3a'] ?? 0;
|
||||
int fgm = s['fgm'] ?? 0; int fga = s['fga'] ?? 0;
|
||||
int ftm = s['ftm'] ?? 0; int fta = s['fta'] ?? 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 orb = s['orb'] ?? 0;
|
||||
int drb = s['drb'] ?? 0;
|
||||
int stl = s['stl'] ?? 0;
|
||||
int ast = s['ast'] ?? 0;
|
||||
int tov = s['tov'] ?? 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 orb = s['orb'] ?? 0; int drb = s['drb'] ?? 0;
|
||||
int stl = s['stl'] ?? 0; int ast = s['ast'] ?? 0;
|
||||
int tov = s['tov'] ?? 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;
|
||||
|
||||
tPts += pts; tFgm += fgm; tFga += fga; tFtm += ftm; tFta += fta;
|
||||
@@ -525,8 +464,7 @@ class PdfExportService {
|
||||
tableData.add([
|
||||
p['number']?.toString() ?? '-',
|
||||
p['name']?.toString() ?? '?',
|
||||
minStr,
|
||||
pts.toString(),
|
||||
minStr, pts.toString(),
|
||||
p2m.toString(), p2a.toString(), p2Pct,
|
||||
p3m.toString(), p3a.toString(), p3Pct,
|
||||
fgm.toString(), fga.toString(), fgPct,
|
||||
@@ -717,8 +655,7 @@ class PdfExportService {
|
||||
);
|
||||
}
|
||||
|
||||
static pw.Widget _groupHeader(
|
||||
String title, pw.TextStyle hStyle, pw.TextStyle sStyle) {
|
||||
static pw.Widget _groupHeader(String title, pw.TextStyle hStyle, pw.TextStyle sStyle) {
|
||||
return pw.Column(
|
||||
children: [
|
||||
pw.Container(
|
||||
@@ -726,54 +663,28 @@ class PdfExportService {
|
||||
alignment: pw.Alignment.center,
|
||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
||||
decoration: const pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)),
|
||||
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)),
|
||||
),
|
||||
child: pw.Text(title, style: hStyle),
|
||||
),
|
||||
pw.Row(children: [
|
||||
pw.Expanded(
|
||||
child: pw.Container(
|
||||
alignment: pw.Alignment.center,
|
||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
||||
child: pw.Text('C', style: sStyle))),
|
||||
pw.Expanded(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.Expanded(
|
||||
child: pw.Container(
|
||||
alignment: pw.Alignment.center,
|
||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
||||
child: pw.Text('T', style: sStyle))),
|
||||
pw.Expanded(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.Expanded(
|
||||
child: pw.Container(
|
||||
alignment: pw.Alignment.center,
|
||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
||||
child: pw.Text('%', style: sStyle))),
|
||||
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('%', style: sStyle))),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static pw.Widget _groupData(
|
||||
String c, String t, String pct, pw.TextStyle style) {
|
||||
static pw.Widget _groupData(String c, String t, String pct, pw.TextStyle style) {
|
||||
return pw.Row(children: [
|
||||
pw.Expanded(
|
||||
child: pw.Container(
|
||||
alignment: pw.Alignment.center,
|
||||
padding: const pw.EdgeInsets.symmetric(vertical: 4),
|
||||
child: pw.Text(c, style: style))),
|
||||
pw.Expanded(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.Expanded(
|
||||
child: pw.Container(
|
||||
alignment: pw.Alignment.center,
|
||||
padding: const pw.EdgeInsets.symmetric(vertical: 4),
|
||||
child: pw.Text(t, style: style))),
|
||||
pw.Expanded(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.Expanded(
|
||||
child: pw.Container(
|
||||
alignment: pw.Alignment.center,
|
||||
padding: const pw.EdgeInsets.symmetric(vertical: 4),
|
||||
child: pw.Text(pct, style: style))),
|
||||
pw.Expanded(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(
|
||||
width: double.infinity,
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: pw.BoxDecoration(
|
||||
color: color,
|
||||
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),
|
||||
),
|
||||
decoration: pw.BoxDecoration(color: color, 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(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||
children: [
|
||||
pw.Container(width: 12, height: 12,
|
||||
decoration: const pw.BoxDecoration(
|
||||
color: PdfColors.green600, shape: pw.BoxShape.circle)),
|
||||
pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.green600, shape: pw.BoxShape.circle)),
|
||||
pw.SizedBox(width: 4),
|
||||
pw.Text('Cesto marcado', style: pw.TextStyle(fontSize: 10)),
|
||||
pw.SizedBox(width: 20),
|
||||
pw.Container(width: 12, height: 12,
|
||||
decoration: const pw.BoxDecoration(
|
||||
color: PdfColors.red600, shape: pw.BoxShape.circle)),
|
||||
pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.red600, shape: pw.BoxShape.circle)),
|
||||
pw.SizedBox(width: 4),
|
||||
pw.Text('Cesto falhado', style: pw.TextStyle(fontSize: 10)),
|
||||
],
|
||||
@@ -817,28 +715,15 @@ class PdfExportService {
|
||||
static pw.Widget _buildSummaryBox(String title, String value) {
|
||||
return pw.Container(
|
||||
width: 120,
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.TableBorder.all(color: PdfColors.black, width: 1),
|
||||
),
|
||||
decoration: pw.BoxDecoration(border: pw.TableBorder.all(color: PdfColors.black, width: 1)),
|
||||
child: pw.Column(children: [
|
||||
pw.Container(
|
||||
width: double.infinity,
|
||||
padding: const pw.EdgeInsets.all(6),
|
||||
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),
|
||||
width: double.infinity, padding: const pw.EdgeInsets.all(6), 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(
|
||||
width: double.infinity,
|
||||
padding: const pw.EdgeInsets.all(8),
|
||||
child: pw.Text(value,
|
||||
style: pw.TextStyle(
|
||||
fontSize: 10, fontWeight: pw.FontWeight.bold),
|
||||
textAlign: pw.TextAlign.center),
|
||||
width: double.infinity, padding: const pw.EdgeInsets.all(8),
|
||||
child: pw.Text(value, style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center),
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:playmaker/classe/theme.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart'; // 👇 IMPORTAÇÃO PARA CACHE
|
||||
import 'package:shared_preferences/shared_preferences.dart'; // 👇 IMPORTAÇÃO PARA MEMÓRIA RÁPIDA
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../utils/size_extension.dart';
|
||||
import 'login.dart';
|
||||
@@ -23,7 +23,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
File? _localImageFile;
|
||||
String? _uploadedImageUrl;
|
||||
bool _isUploadingImage = false;
|
||||
bool _isMemoryLoaded = false; // 👇 VARIÁVEL MÁGICA CONTRA O PISCAR
|
||||
bool _isMemoryLoaded = false;
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
|
||||
@@ -33,16 +33,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_loadUserAvatar();
|
||||
}
|
||||
|
||||
// 👇 LÊ A IMAGEM DA MEMÓRIA INSTANTANEAMENTE E CONFIRMA NA BD
|
||||
Future<void> _loadUserAvatar() async {
|
||||
// 1. Lê da memória rápida primeiro!
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedUrl = prefs.getString('meu_avatar_guardado');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
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) {
|
||||
final urlDoSupabase = data['avatar_url'];
|
||||
|
||||
// Atualiza a memória se a foto na base de dados for diferente
|
||||
if (urlDoSupabase != savedUrl) {
|
||||
await prefs.setString('meu_avatar_guardado', urlDoSupabase);
|
||||
setState(() {
|
||||
@@ -68,7 +65,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
);
|
||||
|
||||
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
|
||||
.from('profiles')
|
||||
@@ -104,7 +103,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
'avatar_url': publicUrl
|
||||
});
|
||||
|
||||
// 👇 MÁGICA: GUARDA LOGO O NOVO URL NA MEMÓRIA PARA A HOME SABER!
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
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) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
@@ -298,29 +295,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
child: ClipOval(
|
||||
child: _isUploadingImage && _localImageFile != null
|
||||
// 1. Mostrar imagem local (galeria) ENQUANTO está a fazer upload
|
||||
? Image.file(_localImageFile!, fit: BoxFit.cover)
|
||||
|
||||
// 2. Antes da memória carregar, fica só o fundo (evita piscar)
|
||||
: !_isMemoryLoaded
|
||||
? const SizedBox()
|
||||
|
||||
// 3. Depois da memória carregar, se houver URL, desenha com Cache!
|
||||
: _uploadedImageUrl != null && _uploadedImageUrl!.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl: _uploadedImageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
fadeInDuration: Duration.zero, // Fica instantâneo!
|
||||
fadeInDuration: Duration.zero,
|
||||
placeholder: (context, url) => const SizedBox(),
|
||||
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),
|
||||
),
|
||||
),
|
||||
|
||||
// ÍCONE DE LÁPIS
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
@@ -335,7 +324,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// LOADING OVERLAY (Enquanto faz o upload)
|
||||
if (_isUploadingImage)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
@@ -364,9 +352,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
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();
|
||||
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();
|
||||
if (ctx.mounted) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.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 '../utils/size_extension.dart';
|
||||
|
||||
@@ -18,9 +19,55 @@ class _StatusPageState extends State<StatusPage> {
|
||||
|
||||
String? _selectedTeamId;
|
||||
String _selectedTeamName = "Selecionar Equipa";
|
||||
String? _selectedTeamLogo;
|
||||
|
||||
String _sortColumn = 'pts';
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final bgColor = Theme.of(context).cardTheme.color ?? Colors.white;
|
||||
@@ -44,7 +91,19 @@ class _StatusPageState extends State<StatusPage> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
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),
|
||||
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) {
|
||||
Map<String, Map<String, dynamic>> aggregated = {};
|
||||
for (var member in members) {
|
||||
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};
|
||||
}
|
||||
for (var row in stats) {
|
||||
@@ -140,13 +198,19 @@ class _StatusPageState extends State<StatusPage> {
|
||||
|
||||
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, Color bgColor, Color textColor) {
|
||||
return Container(
|
||||
color: Colors.transparent,
|
||||
color: Colors.transparent, // 👇 VOLTOU A ESTAR TRANSPARENTE COMO TINHAS ANTES!
|
||||
width: double.infinity,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const ClampingScrollPhysics(), // Mantém-se o Clamping para não puxar mais do que o ecrã
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
||||
child: DataTable(
|
||||
columnSpacing: 25 * context.sf,
|
||||
columnSpacing: 20 * context.sf,
|
||||
horizontalMargin: 16 * context.sf,
|
||||
headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
|
||||
dataRowMaxHeight: 60 * context.sf,
|
||||
dataRowMinHeight: 60 * context.sf,
|
||||
@@ -166,7 +230,6 @@ class _StatusPageState extends State<StatusPage> {
|
||||
DataCell(
|
||||
Row(
|
||||
children: [
|
||||
// 👇 FOTO DO JOGADOR NA TABELA (COM CACHE!) 👇
|
||||
ClipOval(
|
||||
child: Container(
|
||||
width: 30 * context.sf,
|
||||
@@ -215,6 +278,7 @@ class _StatusPageState extends State<StatusPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -247,10 +311,40 @@ class _StatusPageState extends State<StatusPage> {
|
||||
stream: _teamController.teamsStream,
|
||||
builder: (context, snapshot) {
|
||||
final teams = snapshot.data ?? [];
|
||||
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile(
|
||||
title: Text(teams[i]['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
||||
onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); },
|
||||
));
|
||||
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) {
|
||||
final team = teams[i];
|
||||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
@@ -42,10 +42,7 @@ class ActionSubtypeDialog extends StatelessWidget {
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf),
|
||||
),
|
||||
child: Text(title, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
@@ -80,7 +77,7 @@ class ActionSubtypeDialog extends StatelessWidget {
|
||||
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(
|
||||
e.value,
|
||||
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 possibleVictims = victimCourt.where((id) => !id.startsWith("fake_")).toList();
|
||||
|
||||
// Função interna para verificar se o jogador tem de sair
|
||||
void checkFouledOut() {
|
||||
final fouls = controller.playerStats[committerId]?["fls"] ?? 0;
|
||||
final isCourt = isCommitterOpponent ? controller.oppCourt.contains(committerId) : controller.myCourt.contains(committerId);
|
||||
@@ -116,12 +112,12 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
|
||||
if (!context.mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false, // Obriga a fazer a substituição
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => SubstitutionDialog(
|
||||
controller: controller,
|
||||
isOpponent: isCommitterOpponent,
|
||||
sf: sf,
|
||||
forcedStarterId: committerId, // Passamos o jogador que foi expulso
|
||||
forcedStarterId: committerId,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -155,10 +151,7 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
"Quem sofreu a falta?",
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf),
|
||||
),
|
||||
child: Text("Quem sofreu a falta?", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
@@ -190,7 +183,7 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
controller.registerFoul("$prefixCommitter$committerId", foulType, "$prefixVictim$id");
|
||||
checkFouledOut(); // Verifica 5 faltas!
|
||||
checkFouledOut();
|
||||
},
|
||||
child: Container(
|
||||
width: 80 * sf,
|
||||
@@ -234,7 +227,7 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
controller.registerFoul("$prefixCommitter$committerId", foulType, "");
|
||||
checkFouledOut(); // Verifica 5 faltas!
|
||||
checkFouledOut();
|
||||
},
|
||||
)
|
||||
],
|
||||
@@ -282,10 +275,7 @@ void showAssistDialog(BuildContext context, PlacarController controller, bool is
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
"Houve Assistência?",
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf),
|
||||
),
|
||||
child: Text("Houve Assistência?", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
@@ -317,7 +307,11 @@ void showAssistDialog(BuildContext context, PlacarController controller, bool is
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
controller.commitStat("add_ast", "$prefix$id");
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Assistência: $name'), duration: const Duration(seconds: 1), backgroundColor: AppTheme.successGreen));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('Assistência: $name'),
|
||||
duration: const Duration(seconds: 1),
|
||||
backgroundColor: AppTheme.successGreen,
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
width: 75 * sf,
|
||||
@@ -384,134 +378,82 @@ class TopScoreboard extends StatelessWidget {
|
||||
color: AppTheme.placarDarkSurface,
|
||||
borderRadius: BorderRadius.only(
|
||||
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),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildTeamSection(
|
||||
controller.myTeam,
|
||||
controller.myScore,
|
||||
controller.myFouls,
|
||||
controller.myTimeoutsUsed,
|
||||
AppTheme.myTeamBlue,
|
||||
false,
|
||||
sf),
|
||||
_buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, AppTheme.myTeamBlue, false, sf),
|
||||
SizedBox(width: 20 * sf),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 14 * sf, vertical: 4 * sf),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.placarTimerBg,
|
||||
borderRadius: BorderRadius.circular(9 * sf)),
|
||||
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 4 * sf),
|
||||
decoration: BoxDecoration(color: AppTheme.placarTimerBg, borderRadius: BorderRadius.circular(9 * sf)),
|
||||
child: ValueListenableBuilder<Duration>(
|
||||
valueListenable: controller.durationNotifier,
|
||||
builder: (context, duration, child) {
|
||||
String formatTime =
|
||||
"${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
return Text(formatTime,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24 * sf,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: 1.5 * sf));
|
||||
String formatTime = "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
return Text(formatTime, style: TextStyle(color: Colors.white, fontSize: 24 * sf, fontWeight: FontWeight.w900, fontFamily: 'monospace', letterSpacing: 1.5 * sf));
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4 * sf),
|
||||
Text("PERÍODO ${controller.currentQuarter}",
|
||||
style: TextStyle(
|
||||
color: AppTheme.warningAmber,
|
||||
fontSize: 12 * sf,
|
||||
fontWeight: FontWeight.w900)),
|
||||
Text("PERÍODO ${controller.currentQuarter}", style: TextStyle(color: AppTheme.warningAmber, fontSize: 12 * sf, fontWeight: FontWeight.w900)),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 20 * sf),
|
||||
_buildTeamSection(
|
||||
controller.opponentTeam,
|
||||
controller.opponentScore,
|
||||
controller.opponentFouls,
|
||||
controller.opponentTimeoutsUsed,
|
||||
AppTheme.oppTeamRed,
|
||||
true,
|
||||
sf),
|
||||
_buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, AppTheme.oppTeamRed, true, sf),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTeamSection(String name, int score, int fouls, int timeouts,
|
||||
Color color, bool isOpp, double sf) {
|
||||
Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp, double sf) {
|
||||
int displayFouls = fouls > 5 ? 5 : fouls;
|
||||
final timeoutIndicators = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(
|
||||
3,
|
||||
(index) => Container(
|
||||
children: List.generate(3, (index) => Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 2.5 * sf),
|
||||
width: 10 * sf,
|
||||
height: 10 * sf,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: index < timeouts
|
||||
? AppTheme.warningAmber
|
||||
: Colors.grey.shade600,
|
||||
border:
|
||||
Border.all(color: Colors.white54, width: 1.0 * sf)),
|
||||
color: index < timeouts ? AppTheme.warningAmber : Colors.grey.shade600,
|
||||
border: Border.all(color: Colors.white54, width: 1.0 * sf),
|
||||
),
|
||||
)),
|
||||
);
|
||||
List<Widget> content = [
|
||||
Column(children: [
|
||||
_scoreBox(score, color, sf),
|
||||
SizedBox(height: 5 * sf),
|
||||
timeoutIndicators
|
||||
]),
|
||||
Column(children: [_scoreBox(score, color, sf), SizedBox(height: 5 * sf), timeoutIndicators]),
|
||||
SizedBox(width: 12 * sf),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end,
|
||||
crossAxisAlignment: isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(name.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16 * sf,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 1.0 * sf)),
|
||||
Text(name.toUpperCase(), style: TextStyle(color: Colors.white, fontSize: 16 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.0 * sf)),
|
||||
SizedBox(height: 3 * sf),
|
||||
Text("FALTAS: $displayFouls",
|
||||
style: TextStyle(
|
||||
color: displayFouls >= 5
|
||||
? AppTheme.actionMiss
|
||||
: AppTheme.warningAmber,
|
||||
fontSize: 11 * sf,
|
||||
fontWeight: FontWeight.bold)),
|
||||
Text("FALTAS: $displayFouls", style: TextStyle(color: displayFouls >= 5 ? AppTheme.actionMiss : AppTheme.warningAmber, fontSize: 11 * sf, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
)
|
||||
];
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: isOpp ? content : content.reversed.toList());
|
||||
return Row(crossAxisAlignment: CrossAxisAlignment.center, children: isOpp ? content : content.reversed.toList());
|
||||
}
|
||||
|
||||
Widget _scoreBox(int score, Color color, double sf) => Container(
|
||||
width: 45 * sf,
|
||||
height: 35 * sf,
|
||||
width: 45 * sf, height: 35 * sf,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: color, borderRadius: BorderRadius.circular(6 * sf)),
|
||||
child: Text(score.toString(),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20 * sf,
|
||||
fontWeight: FontWeight.w900)),
|
||||
decoration: BoxDecoration(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 {
|
||||
final Color color;
|
||||
final bool isFouledOut;
|
||||
@@ -523,15 +465,8 @@ class ShirtPainter extends CustomPainter {
|
||||
final double h = size.height;
|
||||
final Color shirtColor = isFouledOut ? Colors.grey.shade700 : color;
|
||||
|
||||
final paint = Paint()
|
||||
..color = shirtColor
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final trimPaint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = w * 0.04
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
final paint = Paint()..color = shirtColor..style = PaintingStyle.fill;
|
||||
final trimPaint = Paint()..color = Colors.white..style = PaintingStyle.stroke..strokeWidth = w * 0.04..strokeJoin = StrokeJoin.round;
|
||||
|
||||
final path = Path();
|
||||
path.moveTo(w * 0.32, h * 0.10);
|
||||
@@ -553,13 +488,61 @@ class ShirtPainter extends CustomPainter {
|
||||
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 {
|
||||
final PlacarController controller;
|
||||
final String playerId;
|
||||
final bool isOpponent;
|
||||
final double sf;
|
||||
|
||||
const PlayerCourtCard({super.key, required this.controller, required this.playerId, required this.isOpponent, required this.sf});
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
@@ -571,23 +554,38 @@ class PlayerCourtCard extends StatelessWidget {
|
||||
|
||||
return Draggable<String>(
|
||||
data: "$prefix$playerId",
|
||||
feedback: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(6)),
|
||||
child: Text(realName, style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
// ✅ CORRIGIDO: mostra camisola + número durante o drag
|
||||
feedback: _dragFeedback(number, teamColor),
|
||||
childWhenDragging: Opacity(
|
||||
opacity: 0.35,
|
||||
child: _playerCardUI(number, realName, stats, teamColor, false, false, false, sf),
|
||||
),
|
||||
),
|
||||
childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, realName, stats, teamColor, false, false, sf)),
|
||||
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) {
|
||||
final action = details.data;
|
||||
|
||||
// ── Troca de posição entre jogadores do campo ──────────────────
|
||||
if (action.startsWith("player_my_") || action.startsWith("player_opp_")) {
|
||||
final sameTeam = isOpponent ? action.startsWith("player_opp_") : action.startsWith("player_my_");
|
||||
if (sameTeam && action != "$prefix$playerId") {
|
||||
controller.swapCourtPlayers(action, "$prefix$playerId");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 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(
|
||||
@@ -597,16 +595,11 @@ class PlayerCourtCard extends StatelessWidget {
|
||||
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);
|
||||
}
|
||||
if (isMake) showAssistDialog(context, controller, isOpponent, playerId, sf);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
// ─── NOVO POP-UP PARA AS FALTAS ───
|
||||
else if (action == "add_foul") {
|
||||
} else if (action == "add_foul") {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => ActionSubtypeDialog(
|
||||
@@ -621,14 +614,11 @@ class PlayerCourtCard extends StatelessWidget {
|
||||
sf: sf,
|
||||
onSelected: (foulType) {
|
||||
Navigator.pop(ctx);
|
||||
// Depois de escolher o tipo de falta, abre o pop-up a perguntar quem sofreu
|
||||
showFoulVictimDialog(context, controller, isOpponent, playerId, foulType, sf);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
// ─── POP-UPS PARA TOV, STL E BLK ───
|
||||
else if (action == "add_tov") {
|
||||
} else if (action == "add_tov") {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => ActionSubtypeDialog(
|
||||
@@ -647,8 +637,7 @@ class PlayerCourtCard extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
else if (action == "add_stl") {
|
||||
} else if (action == "add_stl") {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => ActionSubtypeDialog(
|
||||
@@ -664,8 +653,7 @@ class PlayerCourtCard extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
else if (action == "add_blk") {
|
||||
} else if (action == "add_blk") {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => ActionSubtypeDialog(
|
||||
@@ -681,34 +669,55 @@ class PlayerCourtCard extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
// ─── FIM DOS POP-UPS ESPECIAIS ───
|
||||
else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) {
|
||||
} else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) {
|
||||
controller.handleActionDrag(context, action, "$prefix$playerId");
|
||||
}
|
||||
else if (action.startsWith("bench_")) {
|
||||
} else if (action.startsWith("bench_")) {
|
||||
controller.handleSubbing(context, action, playerId, isOpponent);
|
||||
}
|
||||
},
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
bool isSubbing = candidateData.any((data) => data != null && (data.startsWith("bench_my_") || data.startsWith("bench_opp_")));
|
||||
bool isActionHover = candidateData.any((data) => data != null && (data.startsWith("add_") || data.startsWith("sub_") || data.startsWith("miss_")));
|
||||
bool isSwapHover = candidateData.any((data) =>
|
||||
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;
|
||||
Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white;
|
||||
Color borderColor = isFouledOut ? AppTheme.actionMiss : Colors.transparent;
|
||||
|
||||
if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = AppTheme.myTeamBlue; }
|
||||
else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = AppTheme.actionPoints; }
|
||||
if (isSwapHover) {
|
||||
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 displayName = displayNameStr.length > 12 ? "${displayNameStr.substring(0, 10)}..." : displayNameStr;
|
||||
final double shirtSize = 40 * sf;
|
||||
@@ -734,10 +743,7 @@ class PlayerCourtCard extends StatelessWidget {
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: Size(shirtSize, shirtSize),
|
||||
painter: ShirtPainter(
|
||||
color: teamColor,
|
||||
isFouledOut: isFouledOut,
|
||||
),
|
||||
painter: ShirtPainter(color: teamColor, isFouledOut: isFouledOut),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: shirtSize * 0.15),
|
||||
@@ -761,10 +767,24 @@ class PlayerCourtCard extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(displayName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? AppTheme.actionMiss : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)),
|
||||
Text(
|
||||
displayName,
|
||||
style: TextStyle(
|
||||
fontSize: 14 * sf,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isFouledOut ? AppTheme.actionMiss : Colors.black87,
|
||||
decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 1.5 * sf),
|
||||
Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[700], fontWeight: FontWeight.w600)),
|
||||
Text("${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls", style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[500], fontWeight: FontWeight.w600)),
|
||||
Text(
|
||||
"${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)",
|
||||
style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[700], fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(
|
||||
"${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls",
|
||||
style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[500], fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -774,11 +794,15 @@ class PlayerCourtCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// SUBSTITUTION DIALOG
|
||||
// ==============================================================================
|
||||
|
||||
class SubstitutionDialog extends StatefulWidget {
|
||||
final PlacarController controller;
|
||||
final bool isOpponent;
|
||||
final double sf;
|
||||
final String? forcedStarterId; // <--- ADICIONADO PARA EXPULSÕES
|
||||
final String? forcedStarterId;
|
||||
|
||||
const SubstitutionDialog({
|
||||
super.key,
|
||||
@@ -804,26 +828,18 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
||||
Color get teamColor => isOpp ? AppTheme.oppTeamRed : AppTheme.myTeamBlue;
|
||||
String get teamName => isOpp ? ctrl.opponentTeam : ctrl.myTeam;
|
||||
bool get canConfirm => _selectedStarterId != null && _selectedBenchId != null;
|
||||
bool get isForced => widget.forcedStarterId != null; // NOVO
|
||||
bool get isForced => widget.forcedStarterId != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Se for obrigado a sair, já aparece selecionado!
|
||||
if (isForced) {
|
||||
_selectedStarterId = widget.forcedStarterId;
|
||||
}
|
||||
if (isForced) _selectedStarterId = widget.forcedStarterId;
|
||||
}
|
||||
|
||||
void _confirmSwap() {
|
||||
if (!canConfirm) return;
|
||||
final benchPrefix = isOpp ? "bench_opp_" : "bench_my_";
|
||||
ctrl.handleSubbing(
|
||||
context,
|
||||
"$benchPrefix$_selectedBenchId",
|
||||
_selectedStarterId!,
|
||||
isOpp,
|
||||
);
|
||||
ctrl.handleSubbing(context, "$benchPrefix$_selectedBenchId", _selectedStarterId!, isOpp);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@@ -838,7 +854,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1F2E),
|
||||
borderRadius: BorderRadius.circular(14 * sf),
|
||||
border: Border.all(color: isForced ? AppTheme.actionMiss : const Color(0xFF2D3450), width: 2), // Borda vermelha se for expulsão
|
||||
border: Border.all(color: isForced ? AppTheme.actionMiss : const Color(0xFF2D3450), width: 2),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -846,7 +862,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 10 * sf),
|
||||
decoration: BoxDecoration(
|
||||
color: isForced ? AppTheme.actionMiss.withOpacity(0.8) : const Color(0xFF1E2540), // Fundo vermelho no título
|
||||
color: isForced ? AppTheme.actionMiss.withOpacity(0.8) : const Color(0xFF1E2540),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12 * sf)),
|
||||
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()}",
|
||||
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(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Container(
|
||||
@@ -876,10 +892,8 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
||||
isStarter: true,
|
||||
activeColor: activeColor,
|
||||
onTap: (id) {
|
||||
if (isForced) return; // Se for forçado, não deixa clicar/desmarcar o titular
|
||||
setState(() {
|
||||
_selectedStarterId = _selectedStarterId == id ? null : id;
|
||||
});
|
||||
if (isForced) return;
|
||||
setState(() => _selectedStarterId = _selectedStarterId == id ? null : id);
|
||||
},
|
||||
),
|
||||
Divider(color: Colors.white12, height: 1, indent: 10 * sf, endIndent: 10 * sf),
|
||||
@@ -894,12 +908,11 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
||||
if (fouls >= 5) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('🛑 ${ctrl.playerNames[id]} expulso!'),
|
||||
backgroundColor: AppTheme.actionMiss));
|
||||
backgroundColor: AppTheme.actionMiss,
|
||||
));
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedBenchId = _selectedBenchId == id ? null : id;
|
||||
});
|
||||
setState(() => _selectedBenchId = _selectedBenchId == id ? null : id);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
@@ -914,7 +927,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
||||
padding: EdgeInsets.fromLTRB(12 * sf, 0, 12 * sf, 12 * sf),
|
||||
child: Row(
|
||||
children: [
|
||||
if (!isForced) // Esconde o botão de cancelar
|
||||
if (!isForced)
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white12,
|
||||
@@ -950,31 +963,22 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
||||
}
|
||||
|
||||
String _hintText() {
|
||||
if (isForced && _selectedBenchId == null) {
|
||||
return "Um jogador atingiu as 5 faltas. Seleciona um suplente obrigatoriamente.";
|
||||
} else if (_selectedStarterId == null && _selectedBenchId == null) {
|
||||
return "Seleciona um titular e um suplente para fazer a troca";
|
||||
} else if (_selectedStarterId != null && _selectedBenchId == null) {
|
||||
return "Agora seleciona o suplente que vai entrar";
|
||||
} else if (_selectedStarterId == null && _selectedBenchId != null) {
|
||||
return "Agora seleciona o titular que vai sair";
|
||||
} else {
|
||||
if (isForced && _selectedBenchId == null) 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";
|
||||
if (_selectedStarterId != null && _selectedBenchId == null) return "Agora seleciona o suplente que vai entrar";
|
||||
if (_selectedStarterId == null && _selectedBenchId != null) return "Agora seleciona o titular que vai sair";
|
||||
final s = ctrl.playerNames[_selectedStarterId] ?? "";
|
||||
final sNum = ctrl.playerNumbers[_selectedStarterId] ?? "";
|
||||
final b = ctrl.playerNames[_selectedBenchId] ?? "";
|
||||
final bNum = ctrl.playerNumbers[_selectedBenchId] ?? "";
|
||||
return "#$sNum $s ↔ #$bNum $b";
|
||||
}
|
||||
}
|
||||
|
||||
Widget _sectionLabel(String label) => Padding(
|
||||
padding: EdgeInsets.fromLTRB(12 * sf, 8 * sf, 12 * sf, 4 * sf),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
label.toUpperCase(),
|
||||
style: TextStyle(color: Colors.white38, fontSize: 10 * sf, letterSpacing: 0.8, fontWeight: FontWeight.w500),
|
||||
),
|
||||
child: Text(label.toUpperCase(), style: TextStyle(color: Colors.white38, fontSize: 10 * sf, letterSpacing: 0.8, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1021,10 +1025,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: Size(36 * sf, 36 * sf),
|
||||
painter: ShirtPainter(color: shirtColor, isFouledOut: isFouledOut),
|
||||
),
|
||||
CustomPaint(size: Size(36 * sf, 36 * sf), painter: ShirtPainter(color: shirtColor, isFouledOut: isFouledOut)),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 36 * sf * 0.15),
|
||||
child: Text(
|
||||
@@ -1042,12 +1043,7 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 3 * sf),
|
||||
Text(
|
||||
shortName,
|
||||
style: TextStyle(color: Colors.white70, fontSize: 9 * sf, fontWeight: FontWeight.w500),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(shortName, style: TextStyle(color: Colors.white70, fontSize: 9 * sf, fontWeight: FontWeight.w500), overflow: TextOverflow.ellipsis, textAlign: TextAlign.center),
|
||||
if (isFouledOut)
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 2 * sf),
|
||||
@@ -1068,6 +1064,11 @@ class _SubstitutionDialogState extends State<SubstitutionDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// HEATMAP DIALOG
|
||||
// ==============================================================================
|
||||
|
||||
class HeatmapDialog extends StatefulWidget {
|
||||
final List<dynamic> shots;
|
||||
final String myTeamName;
|
||||
@@ -1166,18 +1167,11 @@ class _HeatmapDialogState extends State<HeatmapDialog> {
|
||||
child: Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => setState(() {
|
||||
_selectedTeam = teamName;
|
||||
_selectedPlayerId = 'Todos';
|
||||
_isMapVisible = true;
|
||||
}),
|
||||
onTap: () => setState(() { _selectedTeam = teamName; _selectedPlayerId = 'Todos'; _isMapVisible = true; }),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: teamColor,
|
||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)),
|
||||
),
|
||||
decoration: BoxDecoration(color: teamColor, borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8))),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(teamName.toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
@@ -1205,11 +1199,7 @@ class _HeatmapDialogState extends State<HeatmapDialog> {
|
||||
leading: Icon(Icons.person, color: teamColor),
|
||||
title: Text(pName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)),
|
||||
trailing: Text("$pts Pts", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: teamColor)),
|
||||
onTap: () => setState(() {
|
||||
_selectedTeam = teamName;
|
||||
_selectedPlayerId = pId;
|
||||
_isMapVisible = true;
|
||||
}),
|
||||
onTap: () => setState(() { _selectedTeam = teamName; _selectedPlayerId = pId; _isMapVisible = true; }),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -1247,11 +1237,7 @@ class _HeatmapDialogState extends State<HeatmapDialog> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
|
||||
child: Row(children: [
|
||||
Icon(Icons.arrow_back, color: headerColor, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text("VOLTAR", style: TextStyle(color: headerColor, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
]),
|
||||
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))]),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1274,10 +1260,7 @@ class _HeatmapDialogState extends State<HeatmapDialog> {
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return Stack(
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: Size(constraints.maxWidth, constraints.maxHeight),
|
||||
painter: HeatmapCourtPainter(),
|
||||
),
|
||||
CustomPaint(size: Size(constraints.maxWidth, constraints.maxHeight), painter: HeatmapCourtPainter()),
|
||||
...filteredShots.map((shot) => Positioned(
|
||||
left: (shot.relativeX * constraints.maxWidth) - 8,
|
||||
top: (shot.relativeY * constraints.maxHeight) - 8,
|
||||
@@ -1345,6 +1328,10 @@ class HeatmapCourtPainter extends CustomPainter {
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// PLAY BY PLAY DIALOG
|
||||
// ==============================================================================
|
||||
|
||||
class PlayByPlayDialog extends StatelessWidget {
|
||||
final PlacarController controller;
|
||||
const PlayByPlayDialog({super.key, required this.controller});
|
||||
@@ -1387,6 +1374,10 @@ class PlayByPlayDialog extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// BOX SCORE DIALOG
|
||||
// ==============================================================================
|
||||
|
||||
class BoxScoreDialog extends StatelessWidget {
|
||||
final PlacarController controller;
|
||||
final double sf;
|
||||
@@ -1400,10 +1391,7 @@ class BoxScoreDialog extends StatelessWidget {
|
||||
builder: (context, child) {
|
||||
return Dialog(
|
||||
backgroundColor: AppTheme.placarDarkSurface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12 * sf),
|
||||
side: BorderSide(color: Colors.white24, width: 1 * sf),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12 * sf), side: BorderSide(color: Colors.white24, width: 1 * sf)),
|
||||
insetPadding: EdgeInsets.all(8 * sf),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: SizedBox(
|
||||
@@ -1419,12 +1407,7 @@ class BoxScoreDialog extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("BOX SCORE", style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: Colors.white, size: 24 * sf),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
)
|
||||
IconButton(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['li'] ?? 0).toString(), style: const TextStyle(color: Colors.orangeAccent))),
|
||||
DataCell(Text((s['pa'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))),
|
||||
DataCell(Text((s['tres_seg'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), // CORRIGIDO PARA MOSTRAR OS 3 SEG NO BOX SCORE
|
||||
DataCell(Text((s['tres_seg'] ?? 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))),
|
||||
]);
|
||||
|
||||
@@ -9,6 +9,7 @@ import app_links
|
||||
import file_selector_macos
|
||||
import path_provider_foundation
|
||||
import printing
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
@@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
|
||||
126
pubspec.lock
126
pubspec.lock
@@ -45,10 +45,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.9"
|
||||
version: "3.6.1"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -121,6 +121,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -177,6 +185,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -189,10 +213,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -288,6 +320,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
gotrue:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -304,6 +344,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -324,10 +372,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
version: "4.3.0"
|
||||
image_cropper:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -496,6 +544,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -624,14 +680,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
postgrest:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -656,6 +704,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -672,6 +728,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -688,6 +752,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -997,6 +1077,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: ba7d5750e3441caa1bbe31d9e516348fcf8dfcb32aa29ef87a844a59f4d1f1d0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1013,6 +1101,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1023,4 +1119,4 @@ packages:
|
||||
version: "2.1.0"
|
||||
sdks:
|
||||
dart: ">=3.10.0 <4.0.0"
|
||||
flutter: ">=3.38.0"
|
||||
flutter: ">=3.38.1"
|
||||
|
||||
@@ -43,6 +43,8 @@ dependencies:
|
||||
shared_preferences: ^2.5.4
|
||||
printing: ^5.14.3
|
||||
pdf: ^3.12.0
|
||||
excel: ^4.0.6
|
||||
share_plus: ^13.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <printing/printing_plugin.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
@@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
PrintingPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
file_selector_windows
|
||||
printing
|
||||
share_plus
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user