pdf e exel

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

View File

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

View File

@@ -94,4 +94,4 @@ class GameController {
} }
} }
void dispose() {} void dispose() {}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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