diff --git a/lib/controllers/active_team.dart b/lib/controllers/active_team.dart new file mode 100644 index 0000000..ce6008a --- /dev/null +++ b/lib/controllers/active_team.dart @@ -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 globalActiveTeam = ValueNotifier(null); + +// 🟢 FUNÇÃO PARA CARREGAR A EQUIPA AO ABRIR A APP (Lê da Memória e do Supabase) +Future 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 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"); + } + } +} \ No newline at end of file diff --git a/lib/controllers/game_controller.dart b/lib/controllers/game_controller.dart index aeb37fb..cd82fe7 100644 --- a/lib/controllers/game_controller.dart +++ b/lib/controllers/game_controller.dart @@ -94,4 +94,4 @@ class GameController { } } void dispose() {} -} \ No newline at end of file +} diff --git a/lib/pages/excel_export_service.dart b/lib/pages/excel_export_service.dart new file mode 100644 index 0000000..d5c9719 --- /dev/null +++ b/lib/pages/excel_export_service.dart @@ -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 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> memberInfo = { + for (var m in membersResponse) m['id'].toString(): m + }; + + final teamsResponse = await supabase.from('teams').select('id, name'); + final Map 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 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 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 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> 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 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> 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 colsT1 = ["Nº", "NOME", "MIN", "PTS", "C", "T", "%", "C", "T", "%", "C", "T", "%", "C", "T", "%", "RO", "RD", "TR"]; + for(int i = 0; i < colsT1.length; i++) { + var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)); + cell.value = TextCellValue(colsT1[i]); + + if (i >= 4 && i <= 6) cell.cellStyle = styleGrelha(cor2P, isBold: true); + else if (i >= 7 && i <= 9) cell.cellStyle = styleGrelha(cor3P, isBold: true); + else if (i >= 10 && i <= 12) cell.cellStyle = styleGrelha(corGlobal, isBold: true); + else if (i >= 13 && i <= 15) cell.cellStyle = styleGrelha(corLL, isBold: true); + else if (i >= 16 && i <= 18) cell.cellStyle = styleGrelha(corReb, isBold: true); + else cell.cellStyle = styleGrelha(corFundoCinza, isBold: true); + } + r++; + + for(var p in processedPlayers) { + String minStr = '${p['minSecs'] ~/ 60}:${(p['minSecs'] % 60).toString().padLeft(2, '0')}'; + String p2Pct = p['p2a'] > 0 ? '${((p['p2m'] / p['p2a']) * 100).toStringAsFixed(0)}%' : '-'; + String p3Pct = p['p3a'] > 0 ? '${((p['p3m'] / p['p3a']) * 100).toStringAsFixed(0)}%' : '-'; + String fgPct = p['fga'] > 0 ? '${((p['fgm'] / p['fga']) * 100).toStringAsFixed(0)}%' : '-'; + String ftPct = p['fta'] > 0 ? '${((p['ftm'] / p['fta']) * 100).toStringAsFixed(0)}%' : '-'; + + List 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 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 colsT2 = ["Nº", "NOME", "BR", "AS", "BP", "BLK", "FLS", "SO", "IL", "LI", "PA", "3S", "DR"]; + for(int i = 0; i < colsT2.length; i++) { + var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)); + cell.value = TextCellValue(colsT2[i]); + cell.cellStyle = styleGrelha(corFundoCinza, isBold: true); + } + r++; + + for(var p in processedPlayers) { + List 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 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'); + } + } +} \ No newline at end of file diff --git a/lib/pages/gamePage.dart b/lib/pages/gamePage.dart index 1fd8e21..c678ecf 100644 --- a/lib/pages/gamePage.dart +++ b/lib/pages/gamePage.dart @@ -1,13 +1,15 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; +import '../models/game_model.dart'; import 'package:flutter/material.dart'; import 'package:playmaker/pages/PlacarPage.dart'; import 'package:playmaker/classe/theme.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../controllers/team_controller.dart'; import '../controllers/game_controller.dart'; -import '../models/game_model.dart'; import '../utils/size_extension.dart'; import 'pdf_export_service.dart'; +import 'excel_export_service.dart'; class GameResultCard extends StatelessWidget { final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season; @@ -21,6 +23,67 @@ class GameResultCard extends StatelessWidget { this.myTeamLogo, this.opponentTeamLogo, required this.sf, required this.onDelete, }); + void _showTeamSelectionDialog(BuildContext context, String format) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)), + title: Text('Gerar ${format.toUpperCase()}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf, color: Theme.of(context).colorScheme.onSurface)), + content: Text('De qual equipa pretende exportar as estatísticas?', style: TextStyle(fontSize: 14 * sf, color: Theme.of(context).colorScheme.onSurface)), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(ctx); + _exportDocument(context, format, myTeam); + }, + child: Text(myTeam, style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf)) + ), + TextButton( + onPressed: () { + Navigator.pop(ctx); + _exportDocument(context, format, opponentTeam); + }, + child: Text(opponentTeam, style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf)) + ), + TextButton( + onPressed: () { + Navigator.pop(ctx); + _exportDocument(context, format, 'Ambas'); + }, + child: Text('Ambas', style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * sf)) + ), + ], + ) + ); + } + + Future _exportDocument(BuildContext context, String format, String targetTeam) async { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('A gerar ${format.toUpperCase()}...'), duration: const Duration(seconds: 1))); + + if (format == 'pdf') { + await PdfExportService.generateAndPrintBoxScore( + gameId: gameId, + myTeam: myTeam, + opponentTeam: opponentTeam, + myScore: myScore, + opponentScore: opponentScore, + season: season, + targetTeam: targetTeam, + ); + } else if (format == 'excel') { + await ExcelExportService.generateAndPrintBoxScoreExcel( + gameId: gameId, + myTeam: myTeam, + opponentTeam: opponentTeam, + myScore: myScore, + opponentScore: opponentScore, + season: season, + targetTeam: targetTeam, + ); + } + } + @override Widget build(BuildContext context) { final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface; @@ -46,32 +109,71 @@ class GameResultCard extends StatelessWidget { ], ), + // 👇 MENU DOS 3 PONTOS (MAIS NÍTIDO E MODERNO) Positioned( - top: -10 * sf, - right: -10 * sf, - child: Row( - children: [ - IconButton( - icon: Icon(Icons.picture_as_pdf, color: AppTheme.primaryRed.withOpacity(0.8), size: 22 * sf), - splashRadius: 20 * sf, - tooltip: 'Gerar PDF', - onPressed: () async { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('A gerar PDF...'), duration: Duration(seconds: 1))); - await PdfExportService.generateAndPrintBoxScore( - gameId: gameId, - myTeam: myTeam, - opponentTeam: opponentTeam, - myScore: myScore, - opponentScore: opponentScore, - season: season, - ); - }, + top: -12 * sf, + right: -12 * sf, + child: PopupMenuButton( + icon: Icon(Icons.more_vert, color: Colors.grey.shade600, size: 26 * sf), // Ícone um pouco maior + splashRadius: 24 * sf, + elevation: 8, // Adiciona sombra para não se misturar com o fundo + shadowColor: Colors.black45, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16 * sf)), + color: Theme.of(context).colorScheme.surface, + surfaceTintColor: Theme.of(context).colorScheme.surface, // Previne que o material 3 mude a cor + onSelected: (value) { + if (value == 'pdf' || value == 'excel') { + _showTeamSelectionDialog(context, value); + } else if (value == 'delete') { + _showDeleteConfirmation(context); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'pdf', + child: Row( + children: [ + // Í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( - icon: Icon(Icons.delete_outline, color: Colors.grey.shade400, size: 22 * sf), - splashRadius: 20 * sf, - tooltip: 'Eliminar Jogo', - onPressed: () => _showDeleteConfirmation(context), + PopupMenuItem( + value: 'excel', + child: Row( + children: [ + // Ícone com fundo arredondado + Container( + padding: EdgeInsets.all(8 * sf), + decoration: BoxDecoration(color: Colors.green.shade600.withOpacity(0.1), shape: BoxShape.circle), + child: Icon(Icons.table_chart, color: Colors.green.shade600, size: 20 * sf), + ), + SizedBox(width: 14 * sf), + Text('Gerar Excel', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)), + ], + ), + ), + const PopupMenuDivider(height: 1), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + // Ícone com fundo arredondado + Container( + padding: EdgeInsets.all(8 * sf), + decoration: BoxDecoration(color: Colors.grey.shade500.withOpacity(0.1), shape: BoxShape.circle), + child: Icon(Icons.delete_outline, color: Colors.grey.shade700, size: 20 * sf), + ), + SizedBox(width: 14 * sf), + Text('Eliminar Jogo', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)), + ], + ), ), ], ), diff --git a/lib/pages/home.dart b/lib/pages/home.dart index ac21498..70278b0 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -23,6 +23,7 @@ class _HomeScreenState extends State { final TeamController _teamController = TeamController(); String? _selectedTeamId; String _selectedTeamName = "Selecionar Equipa"; + String? _selectedTeamLogo; int _teamWins = 0; int _teamLosses = 0; @@ -31,47 +32,113 @@ class _HomeScreenState extends State { final _supabase = Supabase.instance.client; String? _avatarUrl; - bool _isMemoryLoaded = false; // A variável mágica que impede o "piscar" inicial + bool _isMemoryLoaded = false; + + // A chave mágica para forçar a StatusPage a atualizar + String _statusKey = 'status_page_inicial'; @override void initState() { super.initState(); _loadUserAvatar(); + _loadSelectedTeam(); + } + + Future _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 _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 _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 _loadUserAvatar() async { - // 1. LÊ DA MEMÓRIA RÁPIDA PRIMEIRO final prefs = await SharedPreferences.getInstance(); final savedUrl = prefs.getString('meu_avatar_guardado'); if (mounted) { setState(() { if (savedUrl != null) _avatarUrl = savedUrl; - _isMemoryLoaded = true; // Avisa o ecrã que a memória já respondeu! + _isMemoryLoaded = true; }); } - // 2. VAI AO SUPABASE VERIFICAR SE TROCASTE DE FOTO final userId = _supabase.auth.currentUser?.id; if (userId == null) return; try { - final data = await _supabase - .from('profiles') - .select('avatar_url') - .eq('id', userId) - .maybeSingle(); - + final data = await _supabase.from('profiles').select('avatar_url').eq('id', userId).maybeSingle(); if (mounted && data != null && data['avatar_url'] != null) { final urlDoSupabase = data['avatar_url']; - - // Se a foto na base de dados for nova, ele guarda e atualiza! if (urlDoSupabase != savedUrl) { await prefs.setString('meu_avatar_guardado', urlDoSupabase); - setState(() { - _avatarUrl = urlDoSupabase; - }); + setState(() { _avatarUrl = urlDoSupabase; }); } } } catch (e) { @@ -85,7 +152,7 @@ class _HomeScreenState extends State { _buildHomeContent(context), const GamePage(), const TeamsPage(), - const StatusPage(), + StatusPage(key: ValueKey(_statusKey)), // A StatusPage recarrega sempre que a chave muda! ]; return Scaffold( @@ -95,55 +162,37 @@ class _HomeScreenState extends State { backgroundColor: AppTheme.primaryRed, foregroundColor: Colors.white, elevation: 0, - leading: Padding( padding: EdgeInsets.all(10.0 * context.sf), child: InkWell( borderRadius: BorderRadius.circular(100), onTap: () async { - await Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), - ); + await Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen())); _loadUserAvatar(); }, - // SÓ MOSTRA A IMAGEM OU O BONECO DEPOIS DE LER A MEMÓRIA child: !_isMemoryLoaded - // Nos primeiros 0.05 segs, mostra só o círculo de fundo (sem boneco) ? CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)) - - // Depois da memória responder: : _avatarUrl != null && _avatarUrl!.isNotEmpty ? CachedNetworkImage( imageUrl: _avatarUrl!, - fadeInDuration: Duration.zero, // Corta o atraso visual! - imageBuilder: (context, imageProvider) => CircleAvatar( - backgroundColor: Colors.white.withOpacity(0.2), - backgroundImage: imageProvider, - ), + fadeInDuration: Duration.zero, + imageBuilder: (context, imageProvider) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), backgroundImage: imageProvider), placeholder: (context, url) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)), - errorWidget: (context, url, error) => CircleAvatar( - backgroundColor: Colors.white.withOpacity(0.2), - child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf), - ), + errorWidget: (context, url, error) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf)), ) - // Se não tiver foto nenhuma, aí sim mostra o boneco - : CircleAvatar( - backgroundColor: Colors.white.withOpacity(0.2), - child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf), - ), + : CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf)), ), ), ), - - body: IndexedStack( - index: _selectedIndex, - children: pages, - ), - + body: IndexedStack(index: _selectedIndex, children: pages), bottomNavigationBar: NavigationBar( selectedIndex: _selectedIndex, - onDestinationSelected: (index) => setState(() => _selectedIndex = index), + onDestinationSelected: (index) { + setState(() => _selectedIndex = index); + if (index == 0) { + _loadSelectedTeam(); + } + }, backgroundColor: Theme.of(context).colorScheme.surface, surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, elevation: 1, @@ -167,13 +216,8 @@ class _HomeScreenState extends State { return StreamBuilder>>( stream: _teamController.teamsStream, builder: (context, snapshot) { - // Correção: Verifica hasData para evitar piscar tela de loading - if (!snapshot.hasData && snapshot.connectionState == ConnectionState.waiting) { - return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); - } - if (!snapshot.hasData || snapshot.data!.isEmpty) { - return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)))); - } + if (!snapshot.hasData && snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); + if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)))); final teams = snapshot.data!; return ListView.builder( @@ -181,18 +225,33 @@ class _HomeScreenState extends State { itemCount: teams.length, itemBuilder: (context, index) { final team = teams[index]; + final String? logoUrl = team['image_url']; + return ListTile( - leading: const Icon(Icons.shield, color: AppTheme.primaryRed), + leading: ClipOval( + child: Container( + width: 36 * context.sf, height: 36 * context.sf, color: AppTheme.primaryRed.withOpacity(0.1), + child: (logoUrl != null && logoUrl.isNotEmpty) + ? CachedNetworkImage(imageUrl: logoUrl, fit: BoxFit.cover, placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf)) + : Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), + ), + ), title: Text(team['name'] ?? 'Sem Nome', style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)), - onTap: () { + onTap: () async { setState(() { _selectedTeamId = team['id'].toString(); _selectedTeamName = team['name'] ?? 'Desconhecido'; + _selectedTeamLogo = logoUrl; _teamWins = int.tryParse(team['wins']?.toString() ?? '0') ?? 0; _teamLosses = int.tryParse(team['losses']?.toString() ?? '0') ?? 0; _teamDraws = int.tryParse(team['draws']?.toString() ?? '0') ?? 0; + + // Dizemos à StatusPage que a equipa mudou alterando a chave! + _statusKey = DateTime.now().toString(); }); - Navigator.pop(context); + + await _saveSelectedTeam(); + if (context.mounted) Navigator.pop(context); }, ); }, @@ -225,16 +284,14 @@ class _HomeScreenState extends State { onTap: () => _showTeamSelector(context), child: Container( padding: EdgeInsets.all(12 * context.sf), - decoration: BoxDecoration( - color: Theme.of(context).cardTheme.color, - borderRadius: BorderRadius.circular(15 * context.sf), - border: Border.all(color: Colors.grey.withOpacity(0.2)) - ), + decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row(children: [ - Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf), + (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) + ? ClipOval(child: CachedNetworkImage(imageUrl: _selectedTeamLogo!, width: 24 * context.sf, height: 24 * context.sf, fit: BoxFit.cover, placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf), errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf))) + : Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf), SizedBox(width: 10 * context.sf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor)) ]), @@ -263,17 +320,7 @@ class _HomeScreenState extends State { children: [ Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statRebBg)), SizedBox(width: 12 * context.sf), - Expanded( - child: PieChartCard( - victories: _teamWins, - defeats: _teamLosses, - draws: _teamDraws, - title: 'DESEMPENHO', - subtitle: 'Temporada', - backgroundColor: AppTheme.statPieBg, - sf: context.sf - ), - ), + Expanded(child: PieChartCard(victories: _teamWins, defeats: _teamLosses, draws: _teamDraws, title: 'DESEMPENHO', subtitle: 'Temporada', backgroundColor: AppTheme.statPieBg, sf: context.sf)), ], ), ), @@ -284,45 +331,16 @@ class _HomeScreenState extends State { _selectedTeamName == "Selecionar Equipa" ? Container( - width: double.infinity, - padding: EdgeInsets.all(24.0 * context.sf), - decoration: BoxDecoration( - color: Theme.of(context).cardTheme.color ?? Colors.white, - borderRadius: BorderRadius.circular(16 * context.sf), - border: Border.all(color: Colors.grey.withOpacity(0.1)), - boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4))], - ), + width: double.infinity, padding: EdgeInsets.all(24.0 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color ?? Colors.white, borderRadius: BorderRadius.circular(16 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4))]), child: Column( children: [ - Container( - padding: EdgeInsets.all(18 * context.sf), - decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.08), shape: BoxShape.circle), - child: Icon(Icons.shield_outlined, color: AppTheme.primaryRed, size: 42 * context.sf), - ), + Container(padding: EdgeInsets.all(18 * context.sf), decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.08), shape: BoxShape.circle), child: Icon(Icons.shield_outlined, color: AppTheme.primaryRed, size: 42 * context.sf)), SizedBox(height: 20 * context.sf), Text("Nenhuma Equipa Ativa", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: textColor)), SizedBox(height: 8 * context.sf), - Text( - "Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.", - textAlign: TextAlign.center, - style: TextStyle(fontSize: 13 * context.sf, color: Colors.grey.shade600, height: 1.4), - ), + Text("Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.", textAlign: TextAlign.center, style: TextStyle(fontSize: 13 * context.sf, color: Colors.grey.shade600, height: 1.4)), SizedBox(height: 24 * context.sf), - SizedBox( - width: double.infinity, - height: 48 * context.sf, - child: ElevatedButton.icon( - onPressed: () => _showTeamSelector(context), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.primaryRed, - foregroundColor: Colors.white, - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf)), - ), - icon: Icon(Icons.touch_app, size: 20 * context.sf), - label: Text("Selecionar Agora", style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.bold)), - ), - ), + SizedBox(width: double.infinity, height: 48 * context.sf, child: ElevatedButton.icon(onPressed: () => _showTeamSelector(context), style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), icon: Icon(Icons.touch_app, size: 20 * context.sf), label: Text("Selecionar Agora", style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.bold)))), ], ), ) @@ -330,11 +348,7 @@ class _HomeScreenState extends State { stream: _supabase.from('games').stream(primaryKey: ['id']).order('game_date', ascending: false), builder: (context, gameSnapshot) { if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red)); - - // Correção: Verifica hasData em vez de ConnectionState para manter a lista na tela enquanto atualiza em plano de fundo - if (!gameSnapshot.hasData && gameSnapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } + if (!gameSnapshot.hasData && gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator()); final todosOsJogos = gameSnapshot.data ?? []; final gamesList = todosOsJogos.where((game) { @@ -344,44 +358,19 @@ class _HomeScreenState extends State { return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado'; }).take(3).toList(); - if (gamesList.isEmpty) { - return Container( - width: double.infinity, - padding: EdgeInsets.all(20 * context.sf), - decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(14)), - alignment: Alignment.center, - child: const Text("Ainda não há jogos terminados.", style: TextStyle(color: Colors.grey)), - ); - } + if (gamesList.isEmpty) return Container(width: double.infinity, padding: EdgeInsets.all(20 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(14)), alignment: Alignment.center, child: const Text("Ainda não há jogos terminados.", style: TextStyle(color: Colors.grey))); return Column( children: gamesList.map((game) { - String dbMyTeam = game['my_team']?.toString() ?? ''; - String dbOppTeam = game['opponent_team']?.toString() ?? ''; - int dbMyScore = int.tryParse(game['my_score']?.toString() ?? '0') ?? 0; - int dbOppScore = int.tryParse(game['opponent_score']?.toString() ?? '0') ?? 0; - + String dbMyTeam = game['my_team']?.toString() ?? ''; String dbOppTeam = game['opponent_team']?.toString() ?? ''; + int dbMyScore = int.tryParse(game['my_score']?.toString() ?? '0') ?? 0; int dbOppScore = int.tryParse(game['opponent_score']?.toString() ?? '0') ?? 0; String opponent; int myScore; int oppScore; - if (dbMyTeam == _selectedTeamName) { - opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore; - } else { - opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore; - } - - 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'; + if (dbMyTeam == _selectedTeamName) { opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore; } else { opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore; } + String rawDate = game['game_date']?.toString() ?? '---'; String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate; + String result = myScore > oppScore ? 'V' : (myScore < oppScore ? 'D' : 'E'); - return _buildGameHistoryCard( - context: context, opponent: opponent, result: result, - myScore: myScore, oppScore: oppScore, date: date, - topPts: game['top_pts_name'] ?? '---', topAst: game['top_ast_name'] ?? '---', - topRbs: game['top_rbs_name'] ?? '---', topDef: game['top_def_name'] ?? '---', mvp: game['mvp_name'] ?? '---', - ); + return _buildGameHistoryCard(context: context, opponent: opponent, result: result, myScore: myScore, oppScore: oppScore, date: date, topPts: game['top_pts_name'] ?? '---', topAst: game['top_ast_name'] ?? '---', topRbs: game['top_rbs_name'] ?? '---', topDef: game['top_def_name'] ?? '---', mvp: game['mvp_name'] ?? '---'); }).toList(), ); }, @@ -404,39 +393,20 @@ class _HomeScreenState extends State { astMap[pid] = (astMap[pid] ?? 0) + (int.tryParse(row['ast']?.toString() ?? '0') ?? 0); rbsMap[pid] = (rbsMap[pid] ?? 0) + (int.tryParse(row['rbs']?.toString() ?? '0') ?? 0); } - - if (ptsMap.isEmpty) { - return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0}; - } - - String getBest(Map 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 map) { - if (map.isEmpty) return 0; - return map.values.reduce((a, b) => a > b ? a : b); - } - - return { - 'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap), - 'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap), - 'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap) - }; + if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0}; + String getBest(Map map) { if (map.isEmpty) return '---'; return namesMap[map.entries.reduce((a, b) => a.value > b.value ? a : b).key] ?? '---'; } + int getBestVal(Map map) { if (map.isEmpty) return 0; return map.values.reduce((a, b) => a > b ? a : b); } + return {'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap), 'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap), 'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)}; } Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) { return Card( - elevation: 4, margin: EdgeInsets.zero, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: AppTheme.warningAmber, width: 2) : BorderSide.none), + elevation: 4, margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: AppTheme.warningAmber, width: 2) : BorderSide.none), child: Container( decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])), child: LayoutBuilder( builder: (context, constraints) { - final double ch = constraints.maxHeight; - final double cw = constraints.maxWidth; + final double ch = constraints.maxHeight; final double cw = constraints.maxWidth; return Padding( padding: EdgeInsets.all(cw * 0.06), child: Column( @@ -444,23 +414,13 @@ class _HomeScreenState extends State { children: [ Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis), SizedBox(height: ch * 0.011), - SizedBox( - width: double.infinity, - child: FittedBox( - fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, - child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)), - ), - ), + SizedBox(width: double.infinity, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)))), const Spacer(), Center(child: FittedBox(fit: BoxFit.scaleDown, child: Text(statValue, style: TextStyle(fontSize: ch * 0.18, fontWeight: FontWeight.bold, color: Colors.white, height: 1.0)))), SizedBox(height: ch * 0.015), Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))), const Spacer(), - Container( - width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035), - decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)), - child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold))) - ), + Container(width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035), decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)), child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold)))), ], ), ); @@ -470,33 +430,20 @@ class _HomeScreenState extends State { ); } - Widget _buildGameHistoryCard({ - required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date, - required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp - }) { - bool isWin = result == 'V'; - bool isDraw = result == 'E'; + Widget _buildGameHistoryCard({required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date, required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp}) { + bool isWin = result == 'V'; bool isDraw = result == 'E'; Color statusColor = isWin ? AppTheme.successGreen : (isDraw ? AppTheme.warningAmber : AppTheme.oppTeamRed); - final bgColor = Theme.of(context).cardTheme.color; - final textColor = Theme.of(context).colorScheme.onSurface; + final bgColor = Theme.of(context).cardTheme.color; final textColor = Theme.of(context).colorScheme.onSurface; return Container( - margin: EdgeInsets.only(bottom: 14 * context.sf), - decoration: BoxDecoration( - color: bgColor, borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))], - ), + margin: EdgeInsets.only(bottom: 14 * context.sf), decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))]), child: Column( children: [ Padding( padding: EdgeInsets.all(14 * context.sf), child: Row( children: [ - Container( - width: 36 * context.sf, height: 36 * context.sf, - decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle), - child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf))), - ), + Container(width: 36 * context.sf, height: 36 * context.sf, decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle), child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)))), SizedBox(width: 14 * context.sf), Expanded( child: Column( @@ -508,14 +455,7 @@ class _HomeScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), maxLines: 1, overflow: TextOverflow.ellipsis)), - Padding( - padding: EdgeInsets.symmetric(horizontal: 8 * context.sf), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), - decoration: BoxDecoration(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), borderRadius: BorderRadius.circular(8)), - child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: textColor)), - ), - ), + Padding(padding: EdgeInsets.symmetric(horizontal: 8 * context.sf), child: Container(padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), decoration: BoxDecoration(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), borderRadius: BorderRadius.circular(8)), child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: textColor)))), Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)), ], ), @@ -527,30 +467,14 @@ class _HomeScreenState extends State { ), Divider(height: 1, color: Colors.grey.withOpacity(0.1), thickness: 1.5), Container( - width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf), - decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))), + width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))), child: Column( children: [ - Row( - children: [ - Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)), - Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef)), - ], - ), + Row(children: [Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)), Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef))]), SizedBox(height: 8 * context.sf), - Row( - children: [ - Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)), - Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs)), - ], - ), + Row(children: [Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)), Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs))]), SizedBox(height: 8 * context.sf), - Row( - children: [ - Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)), - const Expanded(child: SizedBox()), - ], - ), + Row(children: [Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)), const Expanded(child: SizedBox())]), ], ), ) @@ -562,20 +486,9 @@ class _HomeScreenState extends State { Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, String label, String value, {bool isMvp = false}) { return Row( children: [ - Icon(icon, size: 14 * context.sf, color: color), - SizedBox(width: 4 * context.sf), + Icon(icon, size: 14 * context.sf, color: color), SizedBox(width: 4 * context.sf), Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)), - Expanded( - child: Text( - value, - style: TextStyle( - fontSize: 11 * context.sf, - color: isMvp ? AppTheme.warningAmber : Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.bold - ), - maxLines: 1, overflow: TextOverflow.ellipsis - ) - ), + Expanded(child: Text(value, style: TextStyle(fontSize: 11 * context.sf, color: isMvp ? AppTheme.warningAmber : Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)), ], ); } diff --git a/lib/pages/pdf_export_service.dart b/lib/pages/pdf_export_service.dart index 32bae38..ed9d8bf 100644 --- a/lib/pages/pdf_export_service.dart +++ b/lib/pages/pdf_export_service.dart @@ -4,7 +4,6 @@ import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -// Modelo local para os tiros class _ShotDot { final double relX; final double relY; @@ -13,29 +12,22 @@ class _ShotDot { } class PdfExportService { - // ════════════════════════════════════════════════════════════════════════════ - // ENTRY POINT - // ════════════════════════════════════════════════════════════════════════════ - static Future generateAndPrintBoxScore({ required String gameId, required String myTeam, required String opponentTeam, required String myScore, required String opponentScore, - required String season, + required String season, + required String targetTeam, }) async { final supabase = Supabase.instance.client; // ── Jogo ──────────────────────────────────────────────────────────────── - final gameData = - await supabase.from('games').select().eq('id', gameId).single(); + final gameData = await supabase.from('games').select().eq('id', gameId).single(); // ── Equipas ───────────────────────────────────────────────────────────── - final teamsData = await supabase - .from('teams') - .select('id, name') - .inFilter('name', [myTeam, opponentTeam]); + final teamsData = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]); String? myTeamId; for (var t in teamsData) { @@ -44,32 +36,19 @@ class PdfExportService { // ── Jogadores (Apenas a minha equipa) ─────────────────────────────────── List myPlayers = myTeamId != null - ? await supabase - .from('members') - .select() - .eq('team_id', myTeamId) - .eq('type', 'Jogador') + ? await supabase.from('members').select().eq('team_id', myTeamId).eq('type', 'Jogador') : []; // ── Estatísticas ───────────────────────────────────────────────────────── - final statsData = - await supabase.from('player_stats').select().eq('game_id', gameId); + final statsData = await supabase.from('player_stats').select().eq('game_id', gameId); Map> statsMap = {}; for (var s in statsData) { statsMap[s['member_id'].toString()] = s; } - // ── Tiros (para o mapa de calor da minha equipa) ────────────────────── - final shotsData = await supabase - .from('shot_locations') - .select() - .eq('game_id', gameId); - - // IDs da minha equipa - final Set myPlayerIds = - myPlayers.map((p) => p['id'].toString()).toSet(); - - // Separa os tiros: todos da minha equipa, depois por jogador + // ── Tiros ────────────────────── + final shotsData = await supabase.from('shot_locations').select().eq('game_id', gameId); + final Set myPlayerIds = myPlayers.map((p) => p['id'].toString()).toSet(); final List<_ShotDot> myTeamShots = []; final Map> shotsByPlayer = {}; @@ -86,16 +65,14 @@ class PdfExportService { shotsByPlayer.putIfAbsent(memberId, () => []).add(dot); } - // ── Tabela de estatísticas (Apenas a minha equipa) ──────────────────── - List> myTeamTable = - _buildTeamTableData(myPlayers, statsMap); + // ── Tabela de estatísticas ──────────────────── + List> myTeamTable = _buildTeamTableData(myPlayers, statsMap); // ════════════════════════════════════════════════════════════════════════ // CONSTRUÇÃO DO PDF // ════════════════════════════════════════════════════════════════════════ final pdf = pw.Document(); - // ── PÁGINA 1: Box Score ────────────────────────────────────────────── pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4.landscape, @@ -110,71 +87,81 @@ class PdfExportService { children: [ pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text('Relatório Estatístico', - style: pw.TextStyle( - fontSize: 22, - fontWeight: pw.FontWeight.bold)), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text('Relatório Estatístico', style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 10), + pw.Text('Equipa: $myTeam', style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold, color: const PdfColor.fromInt(0xFFA00000))), + ] + ), + pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ - pw.Text('$myTeam vs $opponentTeam', - style: pw.TextStyle( - fontSize: 15, - fontWeight: pw.FontWeight.bold)), - pw.Text('Resultado: $myScore — $opponentScore', - style: const pw.TextStyle(fontSize: 13)), - pw.Text('Época: $season', - style: const pw.TextStyle(fontSize: 11)), + pw.Text('$myTeam vs $opponentTeam', style: pw.TextStyle(fontSize: 15, fontWeight: pw.FontWeight.bold)), + pw.Text('Resultado: $myScore — $opponentScore', style: const pw.TextStyle(fontSize: 13)), + pw.Text('Época: $season', style: const pw.TextStyle(fontSize: 11)), + pw.SizedBox(height: 10), + + // 👇 NOVA TABELA: PONTUAÇÃO POR PERÍODO 👇 + pw.Table.fromTextArray( + context: context, + border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5), + headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 8), + cellStyle: const pw.TextStyle(fontSize: 8), + headerDecoration: const pw.BoxDecoration(color: PdfColors.grey200), + cellAlignment: pw.Alignment.center, + data: >[ + ['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.Text('Pontos e Lançamentos', - style: pw.TextStyle( - fontSize: 10, - fontWeight: pw.FontWeight.bold, - color: PdfColors.grey700)), + pw.Text('Pontos e Lançamentos', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)), pw.SizedBox(height: 2), - _buildPdfTablePart1( - myTeamTable, const PdfColor.fromInt(0xFFA00000)), + _buildPdfTablePart1(myTeamTable, const PdfColor.fromInt(0xFFA00000)), pw.SizedBox(height: 14), - pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)', - style: pw.TextStyle( - fontSize: 10, - fontWeight: pw.FontWeight.bold, - color: PdfColors.grey700)), + pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)), pw.SizedBox(height: 2), - _buildPdfTablePart2( - myTeamTable, const PdfColor.fromInt(0xFFA00000)), + _buildPdfTablePart2(myTeamTable, const PdfColor.fromInt(0xFFA00000)), pw.SizedBox(height: 16), pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - _buildSummaryBox('Melhor Marcador', - gameData['top_pts_name'] ?? '---'), + _buildSummaryBox('Melhor Marcador', gameData['top_pts_name'] ?? '---'), pw.SizedBox(width: 10), - _buildSummaryBox('Melhor Ressaltador', - gameData['top_rbs_name'] ?? '---'), + _buildSummaryBox('Melhor Ressaltador', gameData['top_rbs_name'] ?? '---'), pw.SizedBox(width: 10), - _buildSummaryBox('Melhor Passador', - gameData['top_ast_name'] ?? '---'), + _buildSummaryBox('Melhor Passador', gameData['top_ast_name'] ?? '---'), pw.SizedBox(width: 10), - _buildSummaryBox( - 'MVP', gameData['mvp_name'] ?? '---'), + _buildSummaryBox('MVP', gameData['mvp_name'] ?? '---'), ], ), ], @@ -195,15 +182,13 @@ class PdfExportService { return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - _heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)', - const PdfColor.fromInt(0xFFA00000)), + _heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)', const PdfColor.fromInt(0xFFA00000)), pw.SizedBox(height: 12), pw.Expanded( child: pw.Center( child: pw.CustomPaint( size: const PdfPoint(360, 360), - painter: (canvas, size) => - _paintCourt(canvas, size, myTeamShots), + painter: (canvas, size) => _paintCourt(canvas, size, myTeamShots), ), ), ), @@ -217,7 +202,6 @@ class PdfExportService { } // ── PÁGINAS 3+: Mapa de Calor por Jogador (4 por folha) ────────────── - // 👇 FILTRO ATIVO: Só entra aqui quem tiver tiros na lista "shotsByPlayer"! final activePlayers = myPlayers.where((p) { final pid = p['id'].toString(); return shotsByPlayer[pid] != null && shotsByPlayer[pid]!.isNotEmpty; @@ -280,9 +264,6 @@ class PdfExportService { ); } - // ════════════════════════════════════════════════════════════════════════════ - // WIDGET DO MAPA DE CALOR INDIVIDUAL - // ════════════════════════════════════════════════════════════════════════════ static pw.Widget _buildPlayerHeatmap(dynamic player, List<_ShotDot> shots, Map stats) { final String playerName = player['name']?.toString() ?? 'Jogador'; final String playerNumber = player['number']?.toString() ?? '0'; @@ -320,16 +301,11 @@ class PdfExportService { ); } - // ════════════════════════════════════════════════════════════════════════════ - // CORREÇÃO: DESENHO DO CAMPO E LINHAS ADAPTADAS - // ════════════════════════════════════════════════════════════════════════════ - static void _paintCourt(PdfGraphics canvas, PdfPoint size, List<_ShotDot> shots) { final double w = size.x; final double h = size.y; final double basketX = w / 2; - // Fundo Amarelo (Toda a área) canvas ..setFillColor(const PdfColor.fromInt(0xFFDFAB00)) ..drawRect(0, 0, w, h) @@ -341,7 +317,6 @@ class PdfExportService { final double alturaDoArco = larguraDoArco * 0.30; final double totalArcoHeight = alturaDoArco * 4; - // ── 1. LINHAS BRANCAS ─────────────────────────────────────────────── canvas.setStrokeColor(PdfColors.white); canvas.setLineWidth(2.0); @@ -350,7 +325,6 @@ class PdfExportService { _drawLine(canvas, h, 0, length, margin, length); _drawLine(canvas, h, w - margin, length, w, length); - // Arco 3pts _drawEllipseArc(canvas, h, basketX, length, larguraDoArco, totalArcoHeight / 2, 0, math.pi); double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75)); @@ -361,46 +335,34 @@ class PdfExportService { _drawLine(canvas, h, sXL, sYL, 0, h * 0.85); _drawLine(canvas, h, sXR, sYR, w, h * 0.85); - // ── 2. LINHAS PRETAS ───────────────────────────────────────────────── canvas.setStrokeColor(PdfColors.black); canvas.setLineWidth(1.5); final double pW = w * 0.28; final double pH = h * 0.38; - // Garrafão _drawRect(canvas, h, basketX - pW / 2, 0, pW, pH); - // Círculo Lances Livres final double ftR = pW / 2; _drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, 0, math.pi); - // Tracejado for (int i = 0; i < 10; i++) { _drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, math.pi + (i * 2 * (math.pi / 20)), math.pi / 20); } - // Linhas oblíquas do garrafão _drawLine(canvas, h, basketX - pW / 2, pH, sXL, sYL); _drawLine(canvas, h, basketX + pW / 2, pH, sXR, sYR); - // Meio Campo _drawEllipseArc(canvas, h, basketX, h, w * 0.12, w * 0.12, math.pi, math.pi); - // Cesto e Tabela _drawCircle(canvas, h, basketX, h * 0.12, w * 0.02); _drawLine(canvas, h, basketX - w * 0.08, h * 0.12 - 5, basketX + w * 0.08, h * 0.12 - 5); - // ── 3. TIROS ───────────────────────────────────────────────────────── for (final shot in shots) { final double px = shot.relX * w; final double py = shot.relY * h; - final PdfColor dotColor = shot.isMake ? PdfColors.green600 : PdfColors.red600; - // Desenha Círculo Colorido _fillCircle(canvas, h, px, py, 6, dotColor); - - // Símbolos canvas.setStrokeColor(PdfColors.white); canvas.setLineWidth(1.2); if (shot.isMake) { @@ -413,19 +375,12 @@ class PdfExportService { } } - // ── Helpers com inversão automática do Eixo Y para casar com Flutter ── static void _drawLine(PdfGraphics c, double canvasH, double x1, double y1, double x2, double y2) { c.moveTo(x1, canvasH - y1); c.lineTo(x2, canvasH - y2); c.strokePath(); } - static void _lineRaw(PdfGraphics c, double x1, double y1, double x2, double y2) { - c.moveTo(x1, y1); - c.lineTo(x2, y2); - c.strokePath(); - } - static void _drawRect(PdfGraphics c, double canvasH, double x, double y, double width, double height) { c.drawRect(x, canvasH - (y + height), width, height); c.strokePath(); @@ -460,12 +415,7 @@ class PdfExportService { c.strokePath(); } - // ════════════════════════════════════════════════════════════════════════════ - // TABELAS DE ESTATÍSTICAS - // ════════════════════════════════════════════════════════════════════════════ - - static List> _buildTeamTableData( - List players, Map> statsMap) { + static List> _buildTeamTableData(List players, Map> statsMap) { List> tableData = []; int tPts = 0, tFgm = 0, tFga = 0, tFtm = 0, tFta = 0, tFls = 0; @@ -485,27 +435,16 @@ class PdfExportService { var s = statsMap[id] ?? {}; int pts = s['pts'] ?? 0; - int fgm = s['fgm'] ?? 0; - int fga = s['fga'] ?? 0; - int ftm = s['ftm'] ?? 0; - int fta = s['fta'] ?? 0; - int p2m = s['p2m'] ?? 0; - int p2a = s['p2a'] ?? 0; - int p3m = s['p3m'] ?? 0; - int p3a = s['p3a'] ?? 0; + int fgm = s['fgm'] ?? 0; int fga = s['fga'] ?? 0; + int ftm = s['ftm'] ?? 0; int fta = s['fta'] ?? 0; + int p2m = s['p2m'] ?? 0; int p2a = s['p2a'] ?? 0; + int p3m = s['p3m'] ?? 0; int p3a = s['p3a'] ?? 0; int fls = s['fls'] ?? 0; - int orb = s['orb'] ?? 0; - int drb = s['drb'] ?? 0; - int stl = s['stl'] ?? 0; - int ast = s['ast'] ?? 0; - int tov = s['tov'] ?? 0; - int blk = s['blk'] ?? 0; - int so = s['so'] ?? 0; - int il = s['il'] ?? 0; - int li = s['li'] ?? 0; - int pa = s['pa'] ?? 0; - int tresS = s['tres_seg'] ?? 0; - int dr = s['dr'] ?? 0; + int orb = s['orb'] ?? 0; int drb = s['drb'] ?? 0; + int stl = s['stl'] ?? 0; int ast = s['ast'] ?? 0; + int tov = s['tov'] ?? 0; int blk = s['blk'] ?? 0; + int so = s['so'] ?? 0; int il = s['il'] ?? 0; int li = s['li'] ?? 0; + int pa = s['pa'] ?? 0; int tresS = s['tres_seg'] ?? 0; int dr = s['dr'] ?? 0; int sec = s['minutos_jogados'] ?? 0; tPts += pts; tFgm += fgm; tFga += fga; tFtm += ftm; tFta += fta; @@ -525,8 +464,7 @@ class PdfExportService { tableData.add([ p['number']?.toString() ?? '-', p['name']?.toString() ?? '?', - minStr, - pts.toString(), + minStr, pts.toString(), p2m.toString(), p2a.toString(), p2Pct, p3m.toString(), p3a.toString(), p3Pct, fgm.toString(), fga.toString(), fgPct, @@ -717,8 +655,7 @@ class PdfExportService { ); } - static pw.Widget _groupHeader( - String title, pw.TextStyle hStyle, pw.TextStyle sStyle) { + static pw.Widget _groupHeader(String title, pw.TextStyle hStyle, pw.TextStyle sStyle) { return pw.Column( children: [ pw.Container( @@ -726,54 +663,28 @@ class PdfExportService { alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), decoration: const pw.BoxDecoration( - border: pw.Border( - bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)), + border: pw.Border(bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)), ), child: pw.Text(title, style: hStyle), ), pw.Row(children: [ - pw.Expanded( - child: pw.Container( - alignment: pw.Alignment.center, - padding: const pw.EdgeInsets.symmetric(vertical: 2), - child: pw.Text('C', style: sStyle))), + pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('C', style: sStyle))), pw.Container(width: 0.5, height: 10, color: PdfColors.white), - pw.Expanded( - child: pw.Container( - alignment: pw.Alignment.center, - padding: const pw.EdgeInsets.symmetric(vertical: 2), - child: pw.Text('T', style: sStyle))), + pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('T', style: sStyle))), pw.Container(width: 0.5, height: 10, color: PdfColors.white), - pw.Expanded( - child: pw.Container( - alignment: pw.Alignment.center, - padding: const pw.EdgeInsets.symmetric(vertical: 2), - child: pw.Text('%', style: sStyle))), + pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('%', style: sStyle))), ]), ], ); } - static pw.Widget _groupData( - String c, String t, String pct, pw.TextStyle style) { + static pw.Widget _groupData(String c, String t, String pct, pw.TextStyle style) { return pw.Row(children: [ - pw.Expanded( - child: pw.Container( - alignment: pw.Alignment.center, - padding: const pw.EdgeInsets.symmetric(vertical: 4), - child: pw.Text(c, style: style))), + pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(c, style: style))), pw.Container(width: 0.5, height: 12, color: PdfColors.grey400), - pw.Expanded( - child: pw.Container( - alignment: pw.Alignment.center, - padding: const pw.EdgeInsets.symmetric(vertical: 4), - child: pw.Text(t, style: style))), + pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(t, style: style))), pw.Container(width: 0.5, height: 12, color: PdfColors.grey400), - pw.Expanded( - child: pw.Container( - alignment: pw.Alignment.center, - padding: const pw.EdgeInsets.symmetric(vertical: 4), - child: pw.Text(pct, style: style))), + pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(pct, style: style))), ]); } @@ -781,17 +692,8 @@ class PdfExportService { return pw.Container( width: double.infinity, padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: pw.BoxDecoration( - color: color, - borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6)), - ), - child: pw.Text( - title, - style: pw.TextStyle( - color: PdfColors.white, - fontSize: 14, - fontWeight: pw.FontWeight.bold), - ), + decoration: pw.BoxDecoration(color: color, borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6))), + child: pw.Text(title, style: pw.TextStyle(color: PdfColors.white, fontSize: 14, fontWeight: pw.FontWeight.bold)), ); } @@ -799,15 +701,11 @@ class PdfExportService { return pw.Row( mainAxisAlignment: pw.MainAxisAlignment.center, children: [ - pw.Container(width: 12, height: 12, - decoration: const pw.BoxDecoration( - color: PdfColors.green600, shape: pw.BoxShape.circle)), + pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.green600, shape: pw.BoxShape.circle)), pw.SizedBox(width: 4), pw.Text('Cesto marcado', style: pw.TextStyle(fontSize: 10)), pw.SizedBox(width: 20), - pw.Container(width: 12, height: 12, - decoration: const pw.BoxDecoration( - color: PdfColors.red600, shape: pw.BoxShape.circle)), + pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.red600, shape: pw.BoxShape.circle)), pw.SizedBox(width: 4), pw.Text('Cesto falhado', style: pw.TextStyle(fontSize: 10)), ], @@ -817,28 +715,15 @@ class PdfExportService { static pw.Widget _buildSummaryBox(String title, String value) { return pw.Container( width: 120, - decoration: pw.BoxDecoration( - border: pw.TableBorder.all(color: PdfColors.black, width: 1), - ), + decoration: pw.BoxDecoration(border: pw.TableBorder.all(color: PdfColors.black, width: 1)), child: pw.Column(children: [ pw.Container( - width: double.infinity, - padding: const pw.EdgeInsets.all(6), - color: const PdfColor.fromInt(0xFFA00000), - child: pw.Text(title, - style: pw.TextStyle( - color: PdfColors.white, - fontSize: 9, - fontWeight: pw.FontWeight.bold), - textAlign: pw.TextAlign.center), + width: double.infinity, padding: const pw.EdgeInsets.all(6), color: const PdfColor.fromInt(0xFFA00000), + child: pw.Text(title, style: pw.TextStyle(color: PdfColors.white, fontSize: 9, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center), ), pw.Container( - width: double.infinity, - padding: const pw.EdgeInsets.all(8), - child: pw.Text(value, - style: pw.TextStyle( - fontSize: 10, fontWeight: pw.FontWeight.bold), - textAlign: pw.TextAlign.center), + width: double.infinity, padding: const pw.EdgeInsets.all(8), + child: pw.Text(value, style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center), ), ]), ); diff --git a/lib/pages/settings_screen.dart b/lib/pages/settings_screen.dart index 6f2c492..d659bed 100644 --- a/lib/pages/settings_screen.dart +++ b/lib/pages/settings_screen.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import 'package:playmaker/classe/theme.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:cached_network_image/cached_network_image.dart'; // 👇 IMPORTAÇÃO PARA CACHE -import 'package:shared_preferences/shared_preferences.dart'; // 👇 IMPORTAÇÃO PARA MEMÓRIA RÁPIDA +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../utils/size_extension.dart'; import 'login.dart'; @@ -23,7 +23,7 @@ class _SettingsScreenState extends State { File? _localImageFile; String? _uploadedImageUrl; bool _isUploadingImage = false; - bool _isMemoryLoaded = false; // 👇 VARIÁVEL MÁGICA CONTRA O PISCAR + bool _isMemoryLoaded = false; final supabase = Supabase.instance.client; @@ -33,16 +33,14 @@ class _SettingsScreenState extends State { _loadUserAvatar(); } - // 👇 LÊ A IMAGEM DA MEMÓRIA INSTANTANEAMENTE E CONFIRMA NA BD Future _loadUserAvatar() async { - // 1. Lê da memória rápida primeiro! final prefs = await SharedPreferences.getInstance(); final savedUrl = prefs.getString('meu_avatar_guardado'); if (mounted) { setState(() { if (savedUrl != null) _uploadedImageUrl = savedUrl; - _isMemoryLoaded = true; // Avisa que já leu a memória + _isMemoryLoaded = true; }); } @@ -59,7 +57,6 @@ class _SettingsScreenState extends State { if (mounted && data != null && data['avatar_url'] != null) { final urlDoSupabase = data['avatar_url']; - // Atualiza a memória se a foto na base de dados for diferente if (urlDoSupabase != savedUrl) { await prefs.setString('meu_avatar_guardado', urlDoSupabase); setState(() { @@ -68,7 +65,7 @@ class _SettingsScreenState extends State { } } } catch (e) { - print("Erro ao carregar avatar: $e"); + debugPrint("Erro ao carregar avatar: $e"); } } @@ -95,7 +92,9 @@ class _SettingsScreenState extends State { fileOptions: const FileOptions(cacheControl: '3600', upsert: true) ); - final String publicUrl = supabase.storage.from('avatars').getPublicUrl(storagePath); + // 👇 TRUQUE MÁGICO PARA O AVATAR ATUALIZAR: Adicionar o timestamp ao URL! + final String baseUrl = supabase.storage.from('avatars').getPublicUrl(storagePath); + final String publicUrl = '$baseUrl?v=${DateTime.now().millisecondsSinceEpoch}'; await supabase .from('profiles') @@ -104,7 +103,6 @@ class _SettingsScreenState extends State { 'avatar_url': publicUrl }); - // 👇 MÁGICA: GUARDA LOGO O NOVO URL NA MEMÓRIA PARA A HOME SABER! final prefs = await SharedPreferences.getInstance(); await prefs.setString('meu_avatar_guardado', publicUrl); @@ -280,7 +278,6 @@ class _SettingsScreenState extends State { ); } - // 👇 AVATAR OTIMIZADO: SEM LAG, COM CACHE E MEMÓRIA Widget _buildTappableProfileAvatar(BuildContext context, Color primaryRed) { return GestureDetector( onTap: () { @@ -298,29 +295,21 @@ class _SettingsScreenState extends State { ), child: ClipOval( child: _isUploadingImage && _localImageFile != null - // 1. Mostrar imagem local (galeria) ENQUANTO está a fazer upload ? Image.file(_localImageFile!, fit: BoxFit.cover) - - // 2. Antes da memória carregar, fica só o fundo (evita piscar) : !_isMemoryLoaded ? const SizedBox() - - // 3. Depois da memória carregar, se houver URL, desenha com Cache! : _uploadedImageUrl != null && _uploadedImageUrl!.isNotEmpty ? CachedNetworkImage( imageUrl: _uploadedImageUrl!, fit: BoxFit.cover, - fadeInDuration: Duration.zero, // Fica instantâneo! + fadeInDuration: Duration.zero, placeholder: (context, url) => const SizedBox(), errorWidget: (context, url, error) => Icon(Icons.person, color: primaryRed, size: 36 * context.sf), ) - - // 4. Se não houver URL, mete o boneco : Icon(Icons.person, color: primaryRed, size: 36 * context.sf), ), ), - // ÍCONE DE LÁPIS Positioned( bottom: 0, right: 0, @@ -335,7 +324,6 @@ class _SettingsScreenState extends State { ), ), - // LOADING OVERLAY (Enquanto faz o upload) if (_isUploadingImage) Positioned.fill( child: Container( @@ -364,9 +352,15 @@ class _SettingsScreenState extends State { ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), onPressed: () async { - // Limpa a memória do Avatar ao sair para não aparecer na conta de outra pessoa! + // 👇 AGORA LIMPA A EQUIPA E TUDO DA MEMÓRIA AO SAIR! final prefs = await SharedPreferences.getInstance(); await prefs.remove('meu_avatar_guardado'); + await prefs.remove('last_team_id'); + await prefs.remove('last_team_name'); + await prefs.remove('last_team_logo'); + await prefs.remove('last_team_wins'); + await prefs.remove('last_team_losses'); + await prefs.remove('last_team_draws'); await Supabase.instance.client.auth.signOut(); if (ctx.mounted) { diff --git a/lib/pages/status_page.dart b/lib/pages/status_page.dart index 31bd56b..ffd4db7 100644 --- a/lib/pages/status_page.dart +++ b/lib/pages/status_page.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:playmaker/classe/theme.dart'; -import 'package:cached_network_image/cached_network_image.dart'; // 👇 A MAGIA DO CACHE +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../controllers/team_controller.dart'; import '../utils/size_extension.dart'; @@ -18,9 +19,55 @@ class _StatusPageState extends State { String? _selectedTeamId; String _selectedTeamName = "Selecionar Equipa"; + String? _selectedTeamLogo; + String _sortColumn = 'pts'; bool _isAscending = false; + @override + void initState() { + super.initState(); + _loadSelectedTeam(); + } + + Future _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 _saveSelectedTeam() async { + final prefs = await SharedPreferences.getInstance(); + if (_selectedTeamId != null) { + await prefs.setString('last_team_id', _selectedTeamId!); + await prefs.setString('last_team_name', _selectedTeamName); + if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) { + await prefs.setString('last_team_logo', _selectedTeamLogo!); + } else { + await prefs.remove('last_team_logo'); + } + } + + final userId = _supabase.auth.currentUser?.id; + if (userId != null && _selectedTeamId != null) { + try { + await _supabase.from('profiles').upsert({ + 'id': userId, + 'selected_team_id': _selectedTeamId, + }); + } catch (e) { + debugPrint("Erro ao guardar equipa no Supabase: $e"); + } + } + } + @override Widget build(BuildContext context) { final bgColor = Theme.of(context).cardTheme.color ?? Colors.white; @@ -44,7 +91,19 @@ class _StatusPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row(children: [ - Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf), + (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) + ? ClipOval( + child: CachedNetworkImage( + imageUrl: _selectedTeamLogo!, + width: 24 * context.sf, + height: 24 * context.sf, + fit: BoxFit.cover, + placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf), + errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf), + ), + ) + : Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf), + SizedBox(width: 10 * context.sf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor)) ]), @@ -99,12 +158,11 @@ class _StatusPageState extends State { ); } - // 👇 AGORA GUARDA TAMBÉM O IMAGE_URL DO MEMBRO PARA MOSTRAR NA TABELA List> _aggregateStats(List stats, List games, List members) { Map> aggregated = {}; for (var member in members) { String name = member['name']?.toString() ?? "Desconhecido"; - String? imageUrl = member['image_url']?.toString(); // 👈 CAPTURA A IMAGEM AQUI + String? imageUrl = member['image_url']?.toString(); aggregated[name] = {'name': name, 'image_url': imageUrl, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0}; } for (var row in stats) { @@ -140,78 +198,84 @@ class _StatusPageState extends State { Widget _buildStatsGrid(BuildContext context, List> players, Map teamTotals, Color bgColor, Color textColor) { return Container( - color: Colors.transparent, + color: Colors.transparent, // 👇 VOLTOU A ESTAR TRANSPARENTE COMO TINHAS ANTES! + width: double.infinity, child: SingleChildScrollView( scrollDirection: Axis.vertical, + physics: const BouncingScrollPhysics(), child: SingleChildScrollView( scrollDirection: Axis.horizontal, - child: DataTable( - columnSpacing: 25 * context.sf, - headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface), - dataRowMaxHeight: 60 * context.sf, - dataRowMinHeight: 60 * context.sf, - columns: [ - DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))), - _buildSortableColumn(context, 'J', 'j', textColor), - _buildSortableColumn(context, 'PTS', 'pts', textColor), - _buildSortableColumn(context, 'AST', 'ast', textColor), - _buildSortableColumn(context, 'RBS', 'rbs', textColor), - _buildSortableColumn(context, 'STL', 'stl', textColor), - _buildSortableColumn(context, 'BLK', 'blk', textColor), - _buildSortableColumn(context, 'DEF 🛡️', 'def', textColor), - _buildSortableColumn(context, 'MVP 🏆', 'mvp', textColor), - ], - rows: [ - ...players.map((player) => DataRow(cells: [ - DataCell( - Row( - children: [ - // 👇 FOTO DO JOGADOR NA TABELA (COM CACHE!) 👇 - ClipOval( - child: Container( - width: 30 * context.sf, - height: 30 * context.sf, - color: Colors.grey.withOpacity(0.2), - child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty) - ? CachedNetworkImage( - imageUrl: player['image_url'], - fit: BoxFit.cover, - fadeInDuration: Duration.zero, - 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), + physics: const ClampingScrollPhysics(), // Mantém-se o Clamping para não puxar mais do que o ecrã + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width), + child: DataTable( + columnSpacing: 20 * context.sf, + horizontalMargin: 16 * context.sf, + headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface), + dataRowMaxHeight: 60 * context.sf, + dataRowMinHeight: 60 * context.sf, + columns: [ + DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))), + _buildSortableColumn(context, 'J', 'j', textColor), + _buildSortableColumn(context, 'PTS', 'pts', textColor), + _buildSortableColumn(context, 'AST', 'ast', textColor), + _buildSortableColumn(context, 'RBS', 'rbs', textColor), + _buildSortableColumn(context, 'STL', 'stl', textColor), + _buildSortableColumn(context, 'BLK', 'blk', textColor), + _buildSortableColumn(context, 'DEF 🛡️', 'def', textColor), + _buildSortableColumn(context, 'MVP 🏆', 'mvp', textColor), + ], + rows: [ + ...players.map((player) => DataRow(cells: [ + DataCell( + Row( + children: [ + ClipOval( + child: Container( + width: 30 * context.sf, + height: 30 * context.sf, + color: Colors.grey.withOpacity(0.2), + child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty) + ? CachedNetworkImage( + imageUrl: player['image_url'], + fit: BoxFit.cover, + fadeInDuration: Duration.zero, + 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), - 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)))), - _buildStatCell(context, player['pts'], textColor, isHighlight: true), - _buildStatCell(context, player['ast'], textColor), - _buildStatCell(context, player['rbs'], textColor), - _buildStatCell(context, player['stl'], textColor), - _buildStatCell(context, player['blk'], textColor), - _buildStatCell(context, player['def'], textColor, isBlue: true), - _buildStatCell(context, player['mvp'], textColor, isGold: true), - ])), - DataRow( - color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)), - cells: [ - 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)))), - _buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true), - _buildStatCell(context, teamTotals['ast'], textColor), - _buildStatCell(context, teamTotals['rbs'], textColor), - _buildStatCell(context, teamTotals['stl'], textColor), - _buildStatCell(context, teamTotals['blk'], textColor), - _buildStatCell(context, teamTotals['def'], textColor, isBlue: true), - _buildStatCell(context, teamTotals['mvp'], textColor, isGold: true), - ] - ) - ], + SizedBox(width: 10 * context.sf), + 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)))), + _buildStatCell(context, player['pts'], textColor, isHighlight: true), + _buildStatCell(context, player['ast'], textColor), + _buildStatCell(context, player['rbs'], textColor), + _buildStatCell(context, player['stl'], textColor), + _buildStatCell(context, player['blk'], textColor), + _buildStatCell(context, player['def'], textColor, isBlue: true), + _buildStatCell(context, player['mvp'], textColor, isGold: true), + ])), + DataRow( + color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)), + cells: [ + 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)))), + _buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true), + _buildStatCell(context, teamTotals['ast'], textColor), + _buildStatCell(context, teamTotals['rbs'], textColor), + _buildStatCell(context, teamTotals['stl'], textColor), + _buildStatCell(context, teamTotals['blk'], textColor), + _buildStatCell(context, teamTotals['def'], textColor, isBlue: true), + _buildStatCell(context, teamTotals['mvp'], textColor, isGold: true), + ] + ) + ], + ), ), ), ), @@ -247,10 +311,40 @@ class _StatusPageState extends State { stream: _teamController.teamsStream, builder: (context, snapshot) { final teams = snapshot.data ?? []; - return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile( - title: Text(teams[i]['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), - onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); }, - )); + return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) { + final team = teams[i]; + final logoUrl = team['image_url']; + + return ListTile( + leading: ClipOval( + child: Container( + width: 36 * context.sf, + height: 36 * context.sf, + color: AppTheme.primaryRed.withOpacity(0.1), + child: (logoUrl != null && logoUrl.isNotEmpty) + ? CachedNetworkImage( + imageUrl: logoUrl, + fit: BoxFit.cover, + placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), + errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), + ) + : Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), + ), + ), + title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), + onTap: () async { + setState(() { + _selectedTeamId = team['id'].toString(); + _selectedTeamName = team['name']; + _selectedTeamLogo = logoUrl; + }); + + await _saveSelectedTeam(); + + if (context.mounted) Navigator.pop(context); + }, + ); + }); }, )); } diff --git a/lib/widgets/placar_widgets.dart b/lib/widgets/placar_widgets.dart index f3177c9..0b12e59 100644 --- a/lib/widgets/placar_widgets.dart +++ b/lib/widgets/placar_widgets.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'dart:math' as math; -import '../classe/theme.dart'; -import '../controllers/placar_controller.dart'; +import '../classe/theme.dart'; +import '../controllers/placar_controller.dart'; import 'package:playmaker/zone_map_dialog.dart'; // ============================================================================== @@ -15,18 +15,18 @@ class ActionSubtypeDialog extends StatelessWidget { final Function(String) onSelected; final double sf; const ActionSubtypeDialog({super.key, required this.title, required this.options, required this.onSelected, required this.sf}); - + @override Widget build(BuildContext context) { return Dialog( backgroundColor: Colors.transparent, elevation: 0, child: Container( - width: MediaQuery.of(context).size.width * 0.55, + width: MediaQuery.of(context).size.width * 0.55, decoration: BoxDecoration( color: AppTheme.placarDarkSurface, borderRadius: BorderRadius.circular(12 * sf), - border: Border.all(color: AppTheme.warningAmber, width: 1.5 * sf), + border: Border.all(color: AppTheme.warningAmber, width: 1.5 * sf), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -34,7 +34,7 @@ class ActionSubtypeDialog extends StatelessWidget { Container( padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 12 * sf), decoration: BoxDecoration( - color: AppTheme.placarListCard, + color: AppTheme.placarListCard, borderRadius: BorderRadius.vertical(top: Radius.circular(10 * sf)), ), child: Stack( @@ -42,10 +42,7 @@ class ActionSubtypeDialog extends StatelessWidget { children: [ Align( alignment: Alignment.center, - child: Text( - title, - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf), - ), + child: Text(title, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)), ), Align( alignment: Alignment.centerRight, @@ -72,7 +69,7 @@ class ActionSubtypeDialog extends StatelessWidget { height: 60 * sf, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.placarTimerBg, + backgroundColor: AppTheme.placarTimerBg, foregroundColor: Colors.white, padding: EdgeInsets.all(6 * sf), shape: RoundedRectangleBorder( @@ -80,12 +77,12 @@ class ActionSubtypeDialog extends StatelessWidget { side: BorderSide(color: Colors.white12, width: 1 * sf), ), ), - onPressed: () => onSelected(e.key), // Retorna a chave correta (ex: "tov_3s") + onPressed: () => onSelected(e.key), child: Text( e.value, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12 * sf), textAlign: TextAlign.center, - maxLines: 2, + maxLines: 2, overflow: TextOverflow.ellipsis, ), ), @@ -106,22 +103,21 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo final victimsColor = isCommitterOpponent ? AppTheme.myTeamBlue : AppTheme.oppTeamRed; final possibleVictims = victimCourt.where((id) => !id.startsWith("fake_")).toList(); - // Função interna para verificar se o jogador tem de sair void checkFouledOut() { final fouls = controller.playerStats[committerId]?["fls"] ?? 0; final isCourt = isCommitterOpponent ? controller.oppCourt.contains(committerId) : controller.myCourt.contains(committerId); - + if (fouls >= 5 && isCourt) { Future.delayed(const Duration(milliseconds: 300), () { if (!context.mounted) return; showDialog( context: context, - barrierDismissible: false, // Obriga a fazer a substituição + barrierDismissible: false, builder: (ctx) => SubstitutionDialog( controller: controller, isOpponent: isCommitterOpponent, sf: sf, - forcedStarterId: committerId, // Passamos o jogador que foi expulso + forcedStarterId: committerId, ), ); }); @@ -130,14 +126,14 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo showDialog( context: context, - barrierDismissible: false, + barrierDismissible: false, builder: (ctx) => Dialog( backgroundColor: Colors.transparent, elevation: 0, child: Container( - width: MediaQuery.of(context).size.width * 0.60, + width: MediaQuery.of(context).size.width * 0.60, decoration: BoxDecoration( - color: AppTheme.placarDarkSurface, + color: AppTheme.placarDarkSurface, borderRadius: BorderRadius.circular(12 * sf), border: Border.all(color: AppTheme.warningAmber, width: 1.5 * sf), ), @@ -155,10 +151,7 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo children: [ Align( alignment: Alignment.center, - child: Text( - "Quem sofreu a falta?", - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf), - ), + child: Text("Quem sofreu a falta?", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)), ), Align( alignment: Alignment.centerRight, @@ -185,18 +178,18 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo children: possibleVictims.map((id) { final name = controller.playerNames[id] ?? "Desconhecido"; final number = controller.playerNumbers[id] ?? "0"; - + return InkWell( onTap: () { - Navigator.pop(ctx); - controller.registerFoul("$prefixCommitter$committerId", foulType, "$prefixVictim$id"); - checkFouledOut(); // Verifica 5 faltas! + Navigator.pop(ctx); + controller.registerFoul("$prefixCommitter$committerId", foulType, "$prefixVictim$id"); + checkFouledOut(); }, child: Container( - width: 80 * sf, + width: 80 * sf, padding: EdgeInsets.all(6 * sf), decoration: BoxDecoration( - color: victimsColor.withOpacity(0.2), + color: victimsColor.withOpacity(0.2), border: Border.all(color: victimsColor, width: 1.5 * sf), borderRadius: BorderRadius.circular(12 * sf), ), @@ -232,10 +225,10 @@ void showFoulVictimDialog(BuildContext context, PlacarController controller, boo icon: Icon(Icons.group, color: Colors.white, size: 16 * sf), label: Text("Equipa / Sem Vítima Específica", style: TextStyle(fontSize: 12 * sf)), onPressed: () { - Navigator.pop(ctx); - controller.registerFoul("$prefixCommitter$committerId", foulType, ""); - checkFouledOut(); // Verifica 5 faltas! - }, + Navigator.pop(ctx); + controller.registerFoul("$prefixCommitter$committerId", foulType, ""); + checkFouledOut(); + }, ) ], ), @@ -251,13 +244,13 @@ void showAssistDialog(BuildContext context, PlacarController controller, bool is final teamCourt = isOpponent ? controller.oppCourt : controller.myCourt; final prefix = isOpponent ? "player_opp_" : "player_my_"; final teamColor = isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue; - + final possibleAssistants = teamCourt.where((id) => id != scorerId && !id.startsWith("fake_")).toList(); - if (possibleAssistants.isEmpty) return; + if (possibleAssistants.isEmpty) return; showDialog( context: context, - barrierDismissible: false, + barrierDismissible: false, builder: (ctx) => Dialog( backgroundColor: Colors.transparent, elevation: 0, @@ -282,10 +275,7 @@ void showAssistDialog(BuildContext context, PlacarController controller, bool is children: [ Align( alignment: Alignment.center, - child: Text( - "Houve Assistência?", - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf), - ), + child: Text("Houve Assistência?", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)), ), Align( alignment: Alignment.centerRight, @@ -312,12 +302,16 @@ void showAssistDialog(BuildContext context, PlacarController controller, bool is children: possibleAssistants.map((id) { final name = controller.playerNames[id] ?? "Desconhecido"; final number = controller.playerNumbers[id] ?? "0"; - + return InkWell( onTap: () { - Navigator.pop(ctx); - controller.commitStat("add_ast", "$prefix$id"); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Assistência: $name'), duration: const Duration(seconds: 1), backgroundColor: AppTheme.successGreen)); + Navigator.pop(ctx); + controller.commitStat("add_ast", "$prefix$id"); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Assistência: $name'), + duration: const Duration(seconds: 1), + backgroundColor: AppTheme.successGreen, + )); }, child: Container( width: 75 * sf, @@ -358,7 +352,7 @@ void showAssistDialog(BuildContext context, PlacarController controller, bool is ), icon: Icon(Icons.person_off, color: Colors.white, size: 16 * sf), label: Text("Isolado (Sem assistência)", style: TextStyle(fontSize: 12 * sf)), - onPressed: () => Navigator.pop(ctx), + onPressed: () => Navigator.pop(ctx), ) ], ), @@ -373,9 +367,9 @@ void showAssistDialog(BuildContext context, PlacarController controller, bool is class TopScoreboard extends StatelessWidget { final PlacarController controller; final double sf; - + const TopScoreboard({super.key, required this.controller, required this.sf}); - + @override Widget build(BuildContext context) { return Container( @@ -383,156 +377,97 @@ class TopScoreboard extends StatelessWidget { decoration: BoxDecoration( color: AppTheme.placarDarkSurface, borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(22 * sf), - bottomRight: Radius.circular(22 * sf)), + bottomLeft: Radius.circular(22 * sf), + bottomRight: Radius.circular(22 * sf), + ), border: Border.all(color: Colors.white, width: 2.0 * sf), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - _buildTeamSection( - controller.myTeam, - controller.myScore, - controller.myFouls, - controller.myTimeoutsUsed, - AppTheme.myTeamBlue, - false, - sf), + _buildTeamSection(controller.myTeam, controller.myScore, controller.myFouls, controller.myTimeoutsUsed, AppTheme.myTeamBlue, false, sf), SizedBox(width: 20 * sf), Column( mainAxisSize: MainAxisSize.min, children: [ Container( - padding: EdgeInsets.symmetric( - horizontal: 14 * sf, vertical: 4 * sf), - decoration: BoxDecoration( - color: AppTheme.placarTimerBg, - borderRadius: BorderRadius.circular(9 * sf)), + padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 4 * sf), + decoration: BoxDecoration(color: AppTheme.placarTimerBg, borderRadius: BorderRadius.circular(9 * sf)), child: ValueListenableBuilder( valueListenable: controller.durationNotifier, builder: (context, duration, child) { - String formatTime = - "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}"; - return Text(formatTime, - style: TextStyle( - color: Colors.white, - fontSize: 24 * sf, - fontWeight: FontWeight.w900, - fontFamily: 'monospace', - letterSpacing: 1.5 * sf)); + String formatTime = "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}"; + return Text(formatTime, style: TextStyle(color: Colors.white, fontSize: 24 * sf, fontWeight: FontWeight.w900, fontFamily: 'monospace', letterSpacing: 1.5 * sf)); }, ), ), SizedBox(height: 4 * sf), - Text("PERÍODO ${controller.currentQuarter}", - style: TextStyle( - color: AppTheme.warningAmber, - fontSize: 12 * sf, - fontWeight: FontWeight.w900)), + Text("PERÍODO ${controller.currentQuarter}", style: TextStyle(color: AppTheme.warningAmber, fontSize: 12 * sf, fontWeight: FontWeight.w900)), ], ), SizedBox(width: 20 * sf), - _buildTeamSection( - controller.opponentTeam, - controller.opponentScore, - controller.opponentFouls, - controller.opponentTimeoutsUsed, - AppTheme.oppTeamRed, - true, - sf), + _buildTeamSection(controller.opponentTeam, controller.opponentScore, controller.opponentFouls, controller.opponentTimeoutsUsed, AppTheme.oppTeamRed, true, sf), ], ), ); } - Widget _buildTeamSection(String name, int score, int fouls, int timeouts, - Color color, bool isOpp, double sf) { + Widget _buildTeamSection(String name, int score, int fouls, int timeouts, Color color, bool isOpp, double sf) { int displayFouls = fouls > 5 ? 5 : fouls; final timeoutIndicators = Row( mainAxisSize: MainAxisSize.min, - children: List.generate( - 3, - (index) => Container( - margin: EdgeInsets.symmetric(horizontal: 2.5 * sf), - width: 10 * sf, - height: 10 * sf, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: index < timeouts - ? AppTheme.warningAmber - : Colors.grey.shade600, - border: - Border.all(color: Colors.white54, width: 1.0 * sf)), - )), + children: List.generate(3, (index) => Container( + margin: EdgeInsets.symmetric(horizontal: 2.5 * sf), + width: 10 * sf, + height: 10 * sf, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: index < timeouts ? AppTheme.warningAmber : Colors.grey.shade600, + border: Border.all(color: Colors.white54, width: 1.0 * sf), + ), + )), ); List content = [ - Column(children: [ - _scoreBox(score, color, sf), - SizedBox(height: 5 * sf), - timeoutIndicators - ]), + Column(children: [_scoreBox(score, color, sf), SizedBox(height: 5 * sf), timeoutIndicators]), SizedBox(width: 12 * sf), Column( - crossAxisAlignment: - isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end, + crossAxisAlignment: isOpp ? CrossAxisAlignment.start : CrossAxisAlignment.end, children: [ - Text(name.toUpperCase(), - style: TextStyle( - color: Colors.white, - fontSize: 16 * sf, - fontWeight: FontWeight.w900, - letterSpacing: 1.0 * sf)), + Text(name.toUpperCase(), style: TextStyle(color: Colors.white, fontSize: 16 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.0 * sf)), SizedBox(height: 3 * sf), - Text("FALTAS: $displayFouls", - style: TextStyle( - color: displayFouls >= 5 - ? AppTheme.actionMiss - : AppTheme.warningAmber, - fontSize: 11 * sf, - fontWeight: FontWeight.bold)), + Text("FALTAS: $displayFouls", style: TextStyle(color: displayFouls >= 5 ? AppTheme.actionMiss : AppTheme.warningAmber, fontSize: 11 * sf, fontWeight: FontWeight.bold)), ], ) ]; - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: isOpp ? content : content.reversed.toList()); + return Row(crossAxisAlignment: CrossAxisAlignment.center, children: isOpp ? content : content.reversed.toList()); } Widget _scoreBox(int score, Color color, double sf) => Container( - width: 45 * sf, - height: 35 * sf, - alignment: Alignment.center, - decoration: BoxDecoration( - color: color, borderRadius: BorderRadius.circular(6 * sf)), - child: Text(score.toString(), - style: TextStyle( - color: Colors.white, - fontSize: 20 * sf, - fontWeight: FontWeight.w900)), - ); + width: 45 * sf, height: 35 * sf, + alignment: Alignment.center, + decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(6 * sf)), + child: Text(score.toString(), style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.w900)), + ); } +// ============================================================================== +// SHIRT PAINTER +// ============================================================================== + class ShirtPainter extends CustomPainter { final Color color; final bool isFouledOut; const ShirtPainter({required this.color, this.isFouledOut = false}); - + @override void paint(Canvas canvas, Size size) { final double w = size.width; final double h = size.height; final Color shirtColor = isFouledOut ? Colors.grey.shade700 : color; - - final paint = Paint() - ..color = shirtColor - ..style = PaintingStyle.fill; - - final trimPaint = Paint() - ..color = Colors.white - ..style = PaintingStyle.stroke - ..strokeWidth = w * 0.04 - ..strokeJoin = StrokeJoin.round; - + + final paint = Paint()..color = shirtColor..style = PaintingStyle.fill; + final trimPaint = Paint()..color = Colors.white..style = PaintingStyle.stroke..strokeWidth = w * 0.04..strokeJoin = StrokeJoin.round; + final path = Path(); path.moveTo(w * 0.32, h * 0.10); path.lineTo(w * 0.18, h * 0.10); @@ -544,69 +479,127 @@ class ShirtPainter extends CustomPainter { path.lineTo(w * 0.68, h * 0.10); path.quadraticBezierTo(w * 0.50, h * 0.45, w * 0.32, h * 0.10); path.close(); - + canvas.drawPath(path, paint); canvas.drawPath(path, trimPaint); } - + @override bool shouldRepaint(ShirtPainter old) => old.color != color || old.isFouledOut != isFouledOut; } +// ============================================================================== +// PLAYER COURT CARD — com feedback de camisola e troca de posições +// ============================================================================== + class PlayerCourtCard extends StatelessWidget { final PlacarController controller; - final String playerId; + final String playerId; final bool isOpponent; - final double sf; - - const PlayerCourtCard({super.key, required this.controller, required this.playerId, required this.isOpponent, required this.sf}); - + final double sf; + + const PlayerCourtCard({ + super.key, + required this.controller, + required this.playerId, + required this.isOpponent, + required this.sf, + }); + + // ── Camisola flutuante mostrada durante o drag ───────────────────────────── + Widget _dragFeedback(String number, Color teamColor) { + const double size = 64; + return Material( + color: Colors.transparent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: size, + height: size, + child: Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + size: const Size(size, size), + painter: ShirtPainter(color: teamColor), + ), + Padding( + padding: const EdgeInsets.only(top: size * 0.15), + child: Text( + number, + style: TextStyle( + color: Colors.white, + fontSize: size * 0.38, + fontWeight: FontWeight.w900, + shadows: const [Shadow(color: Colors.black54, blurRadius: 3, offset: Offset(1, 1))], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { - final teamColor = isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue; + final teamColor = isOpponent ? AppTheme.oppTeamRed : AppTheme.myTeamBlue; final realName = controller.playerNames[playerId] ?? "Erro"; final stats = controller.playerStats[playerId]!; final number = controller.playerNumbers[playerId]!; final prefix = isOpponent ? "player_opp_" : "player_my_"; - + return Draggable( - data: "$prefix$playerId", - feedback: Material( - color: Colors.transparent, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration(color: teamColor.withOpacity(0.9), borderRadius: BorderRadius.circular(6)), - child: Text(realName, style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), - ), + data: "$prefix$playerId", + // ✅ CORRIGIDO: mostra camisola + número durante o drag + feedback: _dragFeedback(number, teamColor), + childWhenDragging: Opacity( + opacity: 0.35, + child: _playerCardUI(number, realName, stats, teamColor, false, false, false, sf), ), - childWhenDragging: Opacity(opacity: 0.5, child: _playerCardUI(number, realName, stats, teamColor, false, false, sf)), child: DragTarget( + onWillAcceptWithDetails: (details) { + final data = details.data; + // Jogadores da mesma equipa → troca de posição + if (data.startsWith("player_my_") || data.startsWith("player_opp_")) { + final sameTeam = isOpponent ? data.startsWith("player_opp_") : data.startsWith("player_my_"); + return sameTeam && data != "$prefix$playerId"; + } + return true; // aceita ações normais + }, onAcceptWithDetails: (details) { - final action = details.data; - + final action = details.data; + + // ── Troca de posição entre jogadores do campo ────────────────── + if (action.startsWith("player_my_") || action.startsWith("player_opp_")) { + final sameTeam = isOpponent ? action.startsWith("player_opp_") : action.startsWith("player_my_"); + if (sameTeam && action != "$prefix$playerId") { + controller.swapCourtPlayers(action, "$prefix$playerId"); + } + return; + } + + // ── Ações normais (inalteradas) ──────────────────────────────── if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") { - bool isMake = action.startsWith("add_"); - bool is3Pt = action.endsWith("_3"); - - showDialog( - context: context, - builder: (ctx) => ZoneMapDialog( - playerName: realName, - isMake: isMake, - is3PointAction: is3Pt, - onZoneSelected: (zone, points, relX, relY) { - Navigator.pop(ctx); - controller.registerShotFromPopup(context, action, "$prefix$playerId", zone, points, relX, relY); - - if (isMake) { - showAssistDialog(context, controller, isOpponent, playerId, sf); - } - }, - ), - ); - } - // ─── NOVO POP-UP PARA AS FALTAS ─── - else if (action == "add_foul") { + bool isMake = action.startsWith("add_"); + bool is3Pt = action.endsWith("_3"); + showDialog( + context: context, + builder: (ctx) => ZoneMapDialog( + playerName: realName, + isMake: isMake, + is3PointAction: is3Pt, + onZoneSelected: (zone, points, relX, relY) { + Navigator.pop(ctx); + controller.registerShotFromPopup(context, action, "$prefix$playerId", zone, points, relX, relY); + if (isMake) showAssistDialog(context, controller, isOpponent, playerId, sf); + }, + ), + ); + } else if (action == "add_foul") { showDialog( context: context, builder: (ctx) => ActionSubtypeDialog( @@ -621,14 +614,11 @@ class PlayerCourtCard extends StatelessWidget { sf: sf, onSelected: (foulType) { Navigator.pop(ctx); - // Depois de escolher o tipo de falta, abre o pop-up a perguntar quem sofreu - showFoulVictimDialog(context, controller, isOpponent, playerId, foulType, sf); + showFoulVictimDialog(context, controller, isOpponent, playerId, foulType, sf); }, ), ); - } - // ─── POP-UPS PARA TOV, STL E BLK ─── - else if (action == "add_tov") { + } else if (action == "add_tov") { showDialog( context: context, builder: (ctx) => ActionSubtypeDialog( @@ -641,14 +631,13 @@ class PlayerCourtCard extends StatelessWidget { "tov_badpass": "Passe Ruim", }, sf: sf, - onSelected: (subAction) { + onSelected: (subAction) { Navigator.pop(ctx); - controller.handleActionDrag(context, subAction, "$prefix$playerId"); + controller.handleActionDrag(context, subAction, "$prefix$playerId"); }, ), ); - } - else if (action == "add_stl") { + } else if (action == "add_stl") { showDialog( context: context, builder: (ctx) => ActionSubtypeDialog( @@ -660,12 +649,11 @@ class PlayerCourtCard extends StatelessWidget { sf: sf, onSelected: (subAction) { Navigator.pop(ctx); - controller.handleActionDrag(context, subAction, "$prefix$playerId"); + controller.handleActionDrag(context, subAction, "$prefix$playerId"); }, ), ); - } - else if (action == "add_blk") { + } else if (action == "add_blk") { showDialog( context: context, builder: (ctx) => ActionSubtypeDialog( @@ -675,46 +663,67 @@ class PlayerCourtCard extends StatelessWidget { "blk_suffered": "Sofreu Desarme", }, sf: sf, - onSelected: (subAction) { + onSelected: (subAction) { Navigator.pop(ctx); - controller.handleActionDrag(context, subAction, "$prefix$playerId"); + controller.handleActionDrag(context, subAction, "$prefix$playerId"); }, ), ); - } - // ─── FIM DOS POP-UPS ESPECIAIS ─── - else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) { - controller.handleActionDrag(context, action, "$prefix$playerId"); - } - else if (action.startsWith("bench_")) { - controller.handleSubbing(context, action, playerId, isOpponent); + } else if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) { + controller.handleActionDrag(context, action, "$prefix$playerId"); + } else if (action.startsWith("bench_")) { + controller.handleSubbing(context, action, playerId, isOpponent); } }, builder: (context, candidateData, rejectedData) { - bool isSubbing = candidateData.any((data) => data != null && (data.startsWith("bench_my_") || data.startsWith("bench_opp_"))); - bool isActionHover = candidateData.any((data) => data != null && (data.startsWith("add_") || data.startsWith("sub_") || data.startsWith("miss_"))); - - return _playerCardUI(number, realName, stats, teamColor, isSubbing, isActionHover, sf); + bool isSwapHover = candidateData.any((data) => + data != null && + (data.startsWith("player_my_") || data.startsWith("player_opp_")) && + data != "$prefix$playerId"); + bool isSubbing = candidateData.any((data) => + data != null && (data.startsWith("bench_my_") || data.startsWith("bench_opp_"))); + bool isActionHover = candidateData.any((data) => + data != null && (data.startsWith("add_") || data.startsWith("sub_") || data.startsWith("miss_"))); + + return _playerCardUI(number, realName, stats, teamColor, isSubbing, isActionHover, isSwapHover, sf); }, ), ); } - - Widget _playerCardUI(String number, String displayNameStr, Map stats, Color teamColor, bool isSubbing, bool isActionHover, double sf) { - bool isFouledOut = stats["fls"]! >= 5; + + Widget _playerCardUI( + String number, + String displayNameStr, + Map stats, + Color teamColor, + bool isSubbing, + bool isActionHover, + bool isSwapHover, + double sf, + ) { + bool isFouledOut = stats["fls"]! >= 5; Color bgColor = isFouledOut ? Colors.red.shade100 : Colors.white; Color borderColor = isFouledOut ? AppTheme.actionMiss : Colors.transparent; - - if (isSubbing) { bgColor = Colors.blue.shade50; borderColor = AppTheme.myTeamBlue; } - else if (isActionHover && !isFouledOut) { bgColor = Colors.orange.shade50; borderColor = AppTheme.actionPoints; } - - int fgm = stats["fgm"]!; int fga = stats["fga"]!; + + if (isSwapHover) { + bgColor = Colors.green.shade50; + borderColor = Colors.green.shade600; + } else if (isSubbing) { + bgColor = Colors.blue.shade50; + borderColor = AppTheme.myTeamBlue; + } else if (isActionHover && !isFouledOut) { + bgColor = Colors.orange.shade50; + borderColor = AppTheme.actionPoints; + } + + int fgm = stats["fgm"]!; + int fga = stats["fga"]!; String fgPercent = fga > 0 ? ((fgm / fga) * 100).toStringAsFixed(0) : "0"; String displayName = displayNameStr.length > 12 ? "${displayNameStr.substring(0, 10)}..." : displayNameStr; final double shirtSize = 40 * sf; - + return Container( - padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 6 * sf), + padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 6 * sf), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(8 * sf), @@ -724,7 +733,7 @@ class PlayerCourtCard extends StatelessWidget { child: IntrinsicHeight( child: Row( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: shirtSize, @@ -734,13 +743,10 @@ class PlayerCourtCard extends StatelessWidget { children: [ CustomPaint( size: Size(shirtSize, shirtSize), - painter: ShirtPainter( - color: teamColor, - isFouledOut: isFouledOut, - ), + painter: ShirtPainter(color: teamColor, isFouledOut: isFouledOut), ), Padding( - padding: EdgeInsets.only(top: shirtSize * 0.15), + padding: EdgeInsets.only(top: shirtSize * 0.15), child: Text( number, style: TextStyle( @@ -755,16 +761,30 @@ class PlayerCourtCard extends StatelessWidget { ], ), ), - SizedBox(width: 8 * sf), + SizedBox(width: 8 * sf), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Text(displayName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold, color: isFouledOut ? AppTheme.actionMiss : Colors.black87, decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none)), - SizedBox(height: 1.5 * sf), - Text("${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[700], fontWeight: FontWeight.w600)), - Text("${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls", style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[500], fontWeight: FontWeight.w600)), + Text( + displayName, + style: TextStyle( + fontSize: 14 * sf, + fontWeight: FontWeight.bold, + color: isFouledOut ? AppTheme.actionMiss : Colors.black87, + decoration: isFouledOut ? TextDecoration.lineThrough : TextDecoration.none, + ), + ), + SizedBox(height: 1.5 * sf), + Text( + "${stats["pts"]} Pts | FG: $fgm/$fga ($fgPercent%)", + style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[700], fontWeight: FontWeight.w600), + ), + Text( + "${stats["ast"]} Ast | ${stats["orb"]! + stats["drb"]!} Rbs | ${stats["fls"]} Fls", + style: TextStyle(fontSize: 10 * sf, color: isFouledOut ? AppTheme.actionMiss : Colors.grey[500], fontWeight: FontWeight.w600), + ), ], ), ], @@ -774,12 +794,16 @@ class PlayerCourtCard extends StatelessWidget { } } +// ============================================================================== +// SUBSTITUTION DIALOG +// ============================================================================== + class SubstitutionDialog extends StatefulWidget { final PlacarController controller; final bool isOpponent; final double sf; - final String? forcedStarterId; // <--- ADICIONADO PARA EXPULSÕES - + final String? forcedStarterId; + const SubstitutionDialog({ super.key, required this.controller, @@ -795,7 +819,7 @@ class SubstitutionDialog extends StatefulWidget { class _SubstitutionDialogState extends State { String? _selectedStarterId; String? _selectedBenchId; - + PlacarController get ctrl => widget.controller; bool get isOpp => widget.isOpponent; double get sf => widget.sf; @@ -804,26 +828,18 @@ class _SubstitutionDialogState extends State { Color get teamColor => isOpp ? AppTheme.oppTeamRed : AppTheme.myTeamBlue; String get teamName => isOpp ? ctrl.opponentTeam : ctrl.myTeam; bool get canConfirm => _selectedStarterId != null && _selectedBenchId != null; - bool get isForced => widget.forcedStarterId != null; // NOVO + bool get isForced => widget.forcedStarterId != null; @override void initState() { super.initState(); - // Se for obrigado a sair, já aparece selecionado! - if (isForced) { - _selectedStarterId = widget.forcedStarterId; - } + if (isForced) _selectedStarterId = widget.forcedStarterId; } - + void _confirmSwap() { if (!canConfirm) return; final benchPrefix = isOpp ? "bench_opp_" : "bench_my_"; - ctrl.handleSubbing( - context, - "$benchPrefix$_selectedBenchId", - _selectedStarterId!, - isOpp, - ); + ctrl.handleSubbing(context, "$benchPrefix$_selectedBenchId", _selectedStarterId!, isOpp); Navigator.pop(context); } @@ -838,7 +854,7 @@ class _SubstitutionDialogState extends State { decoration: BoxDecoration( color: const Color(0xFF1A1F2E), borderRadius: BorderRadius.circular(14 * sf), - border: Border.all(color: isForced ? AppTheme.actionMiss : const Color(0xFF2D3450), width: 2), // Borda vermelha se for expulsão + border: Border.all(color: isForced ? AppTheme.actionMiss : const Color(0xFF2D3450), width: 2), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -846,7 +862,7 @@ class _SubstitutionDialogState extends State { Container( padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 10 * sf), decoration: BoxDecoration( - color: isForced ? AppTheme.actionMiss.withOpacity(0.8) : const Color(0xFF1E2540), // Fundo vermelho no título + color: isForced ? AppTheme.actionMiss.withOpacity(0.8) : const Color(0xFF1E2540), borderRadius: BorderRadius.vertical(top: Radius.circular(12 * sf)), border: const Border(bottom: BorderSide(color: Color(0xFF2D3450))), ), @@ -857,7 +873,7 @@ class _SubstitutionDialogState extends State { isForced ? "SUBSTITUIÇÃO OBRIGATÓRIA (5 Faltas)" : "Substituição — ${teamName.toUpperCase()}", style: TextStyle(color: Colors.white, fontSize: 13 * sf, fontWeight: FontWeight.w600), ), - if (!isForced) // Esconde o "X" de fechar se for forçado + if (!isForced) InkWell( onTap: () => Navigator.pop(context), child: Container( @@ -876,10 +892,8 @@ class _SubstitutionDialogState extends State { isStarter: true, activeColor: activeColor, onTap: (id) { - if (isForced) return; // Se for forçado, não deixa clicar/desmarcar o titular - setState(() { - _selectedStarterId = _selectedStarterId == id ? null : id; - }); + if (isForced) return; + setState(() => _selectedStarterId = _selectedStarterId == id ? null : id); }, ), Divider(color: Colors.white12, height: 1, indent: 10 * sf, endIndent: 10 * sf), @@ -893,13 +907,12 @@ class _SubstitutionDialogState extends State { final fouls = ctrl.playerStats[id]?["fls"] ?? 0; if (fouls >= 5) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('🛑 ${ctrl.playerNames[id]} expulso!'), - backgroundColor: AppTheme.actionMiss)); + content: Text('🛑 ${ctrl.playerNames[id]} expulso!'), + backgroundColor: AppTheme.actionMiss, + )); return; } - setState(() { - _selectedBenchId = _selectedBenchId == id ? null : id; - }); + setState(() => _selectedBenchId = _selectedBenchId == id ? null : id); }, ), Padding( @@ -914,7 +927,7 @@ class _SubstitutionDialogState extends State { padding: EdgeInsets.fromLTRB(12 * sf, 0, 12 * sf, 12 * sf), child: Row( children: [ - if (!isForced) // Esconde o botão de cancelar + if (!isForced) ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.white12, @@ -948,36 +961,27 @@ class _SubstitutionDialogState extends State { ), ); } - + String _hintText() { - if (isForced && _selectedBenchId == null) { - return "Um jogador atingiu as 5 faltas. Seleciona um suplente obrigatoriamente."; - } else if (_selectedStarterId == null && _selectedBenchId == null) { - return "Seleciona um titular e um suplente para fazer a troca"; - } else if (_selectedStarterId != null && _selectedBenchId == null) { - return "Agora seleciona o suplente que vai entrar"; - } else if (_selectedStarterId == null && _selectedBenchId != null) { - return "Agora seleciona o titular que vai sair"; - } else { - final s = ctrl.playerNames[_selectedStarterId] ?? ""; - final sNum = ctrl.playerNumbers[_selectedStarterId] ?? ""; - final b = ctrl.playerNames[_selectedBenchId] ?? ""; - final bNum = ctrl.playerNumbers[_selectedBenchId] ?? ""; - return "#$sNum $s ↔ #$bNum $b"; - } + if (isForced && _selectedBenchId == null) return "Um jogador atingiu as 5 faltas. Seleciona um suplente obrigatoriamente."; + if (_selectedStarterId == null && _selectedBenchId == null) return "Seleciona um titular e um suplente para fazer a troca"; + if (_selectedStarterId != null && _selectedBenchId == null) return "Agora seleciona o suplente que vai entrar"; + if (_selectedStarterId == null && _selectedBenchId != null) return "Agora seleciona o titular que vai sair"; + final s = ctrl.playerNames[_selectedStarterId] ?? ""; + final sNum = ctrl.playerNumbers[_selectedStarterId] ?? ""; + final b = ctrl.playerNames[_selectedBenchId] ?? ""; + final bNum = ctrl.playerNumbers[_selectedBenchId] ?? ""; + return "#$sNum $s ↔ #$bNum $b"; } - + Widget _sectionLabel(String label) => Padding( - padding: EdgeInsets.fromLTRB(12 * sf, 8 * sf, 12 * sf, 4 * sf), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - label.toUpperCase(), - style: TextStyle(color: Colors.white38, fontSize: 10 * sf, letterSpacing: 0.8, fontWeight: FontWeight.w500), - ), - ), - ); - + padding: EdgeInsets.fromLTRB(12 * sf, 8 * sf, 12 * sf, 4 * sf), + child: Align( + alignment: Alignment.centerLeft, + child: Text(label.toUpperCase(), style: TextStyle(color: Colors.white38, fontSize: 10 * sf, letterSpacing: 0.8, fontWeight: FontWeight.w500)), + ), + ); + Widget _playerGrid({ required List players, required String? selected, @@ -1000,7 +1004,7 @@ class _SubstitutionDialogState extends State { final bgColor = isSelected ? const Color(0xFF14331F) : isStarter ? const Color(0xFF1E2540) : const Color(0xFF141824); final borderColor = isSelected ? activeColor : Colors.transparent; final shirtColor = isSelected ? const Color(0xFF15803D) : isFouledOut ? Colors.grey.shade700 : teamColor; - + return GestureDetector( onTap: () => onTap(id), child: AnimatedContainer( @@ -1021,10 +1025,7 @@ class _SubstitutionDialogState extends State { child: Stack( alignment: Alignment.center, children: [ - CustomPaint( - size: Size(36 * sf, 36 * sf), - painter: ShirtPainter(color: shirtColor, isFouledOut: isFouledOut), - ), + CustomPaint(size: Size(36 * sf, 36 * sf), painter: ShirtPainter(color: shirtColor, isFouledOut: isFouledOut)), Padding( padding: EdgeInsets.only(top: 36 * sf * 0.15), child: Text( @@ -1042,12 +1043,7 @@ class _SubstitutionDialogState extends State { ), ), SizedBox(height: 3 * sf), - Text( - shortName, - style: TextStyle(color: Colors.white70, fontSize: 9 * sf, fontWeight: FontWeight.w500), - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - ), + Text(shortName, style: TextStyle(color: Colors.white70, fontSize: 9 * sf, fontWeight: FontWeight.w500), overflow: TextOverflow.ellipsis, textAlign: TextAlign.center), if (isFouledOut) Container( margin: EdgeInsets.only(top: 2 * sf), @@ -1068,6 +1064,11 @@ class _SubstitutionDialogState extends State { ); } } + +// ============================================================================== +// HEATMAP DIALOG +// ============================================================================== + class HeatmapDialog extends StatefulWidget { final List shots; final String myTeamName; @@ -1076,7 +1077,7 @@ class HeatmapDialog extends StatefulWidget { final List oppPlayersIds; final Map> playerStats; final Map playerNames; - + const HeatmapDialog({ super.key, required this.shots, @@ -1087,7 +1088,7 @@ class HeatmapDialog extends StatefulWidget { required this.playerStats, required this.playerNames, }); - + @override State createState() => _HeatmapDialogState(); } @@ -1096,7 +1097,7 @@ class _HeatmapDialogState extends State { bool _isMapVisible = false; String _selectedTeam = ''; String _selectedPlayerId = ''; - + @override Widget build(BuildContext context) { const Color headerColor = Color(0xFFE88F15); @@ -1104,7 +1105,7 @@ class _HeatmapDialogState extends State { final double screenHeight = MediaQuery.of(context).size.height; final double dialogHeight = screenHeight * 0.95; final double dialogWidth = dialogHeight * 1.0; - + return Dialog( backgroundColor: yellowBackground, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), @@ -1117,7 +1118,7 @@ class _HeatmapDialogState extends State { ), ); } - + Widget _buildSelectionScreen(Color headerColor) { return Column( children: [ @@ -1158,7 +1159,7 @@ class _HeatmapDialogState extends State { ], ); } - + Widget _buildTeamColumn({required String teamName, required List playerIds, required Color teamColor}) { final realPlayerIds = playerIds.where((id) => !id.startsWith("fake_")).toList(); return Container( @@ -1166,18 +1167,11 @@ class _HeatmapDialogState extends State { child: Column( children: [ InkWell( - onTap: () => setState(() { - _selectedTeam = teamName; - _selectedPlayerId = 'Todos'; - _isMapVisible = true; - }), + onTap: () => setState(() { _selectedTeam = teamName; _selectedPlayerId = 'Todos'; _isMapVisible = true; }), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 10), - decoration: BoxDecoration( - color: teamColor, - borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)), - ), + decoration: BoxDecoration(color: teamColor, borderRadius: const BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8))), child: Column( children: [ Text(teamName.toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)), @@ -1205,11 +1199,7 @@ class _HeatmapDialogState extends State { leading: Icon(Icons.person, color: teamColor), title: Text(pName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)), trailing: Text("$pts Pts", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: teamColor)), - onTap: () => setState(() { - _selectedTeam = teamName; - _selectedPlayerId = pId; - _isMapVisible = true; - }), + onTap: () => setState(() { _selectedTeam = teamName; _selectedPlayerId = pId; _isMapVisible = true; }), ); }, ), @@ -1218,7 +1208,7 @@ class _HeatmapDialogState extends State { ), ); } - + Widget _buildMapScreen(Color headerColor) { final filteredShots = widget.shots.where((s) { if (_selectedPlayerId != 'Todos') return s.playerId == _selectedPlayerId; @@ -1226,11 +1216,11 @@ class _HeatmapDialogState extends State { if (_selectedTeam == widget.oppTeamName) return widget.oppPlayersIds.contains(s.playerId); return true; }).toList(); - + final titleText = _selectedPlayerId == 'Todos' ? "MAPA GERAL: ${_selectedTeam.toUpperCase()}" : "MAPA: ${widget.playerNames[_selectedPlayerId]?.toUpperCase() ?? ''}"; - + return Column( children: [ Container( @@ -1247,11 +1237,7 @@ class _HeatmapDialogState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), - child: Row(children: [ - Icon(Icons.arrow_back, color: headerColor, size: 14), - const SizedBox(width: 4), - Text("VOLTAR", style: TextStyle(color: headerColor, fontWeight: FontWeight.bold, fontSize: 12)), - ]), + child: Row(children: [Icon(Icons.arrow_back, color: headerColor, size: 14), const SizedBox(width: 4), Text("VOLTAR", style: TextStyle(color: headerColor, fontWeight: FontWeight.bold, fontSize: 12))]), ), ), ), @@ -1274,19 +1260,16 @@ class _HeatmapDialogState extends State { child: LayoutBuilder(builder: (context, constraints) { return Stack( children: [ - CustomPaint( - size: Size(constraints.maxWidth, constraints.maxHeight), - painter: HeatmapCourtPainter(), - ), + CustomPaint(size: Size(constraints.maxWidth, constraints.maxHeight), painter: HeatmapCourtPainter()), ...filteredShots.map((shot) => Positioned( - left: (shot.relativeX * constraints.maxWidth) - 8, - top: (shot.relativeY * constraints.maxHeight) - 8, - child: CircleAvatar( - radius: 8, - backgroundColor: shot.isMake ? AppTheme.successGreen : AppTheme.actionMiss, - child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white), - ), - )), + left: (shot.relativeX * constraints.maxWidth) - 8, + top: (shot.relativeY * constraints.maxHeight) - 8, + child: CircleAvatar( + radius: 8, + backgroundColor: shot.isMake ? AppTheme.successGreen : AppTheme.actionMiss, + child: Icon(shot.isMake ? Icons.check : Icons.close, size: 10, color: Colors.white), + ), + )), ], ); }), @@ -1309,46 +1292,50 @@ class HeatmapCourtPainter extends CustomPainter { final double larguraDoArco = (w / 2) - margin; final double alturaDoArco = larguraDoArco * 0.30; final double totalArcoHeight = alturaDoArco * 4; - + canvas.drawLine(Offset(margin, 0), Offset(margin, length), whiteStroke); canvas.drawLine(Offset(w - margin, 0), Offset(w - margin, length), whiteStroke); canvas.drawLine(Offset(0, length), Offset(margin, length), whiteStroke); canvas.drawLine(Offset(w - margin, length), Offset(w, length), whiteStroke); canvas.drawArc(Rect.fromCenter(center: Offset(basketX, length), width: larguraDoArco * 2, height: totalArcoHeight), 0, math.pi, false, whiteStroke); - + double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75)); double sYL = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.75)); double sXR = basketX + (larguraDoArco * math.cos(math.pi * 0.25)); double sYR = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.25)); - + canvas.drawLine(Offset(sXL, sYL), Offset(0, h * 0.85), whiteStroke); canvas.drawLine(Offset(sXR, sYR), Offset(w, h * 0.85), whiteStroke); - + final double pW = w * 0.28; final double pH = h * 0.38; canvas.drawRect(Rect.fromLTWH(basketX - pW / 2, 0, pW, pH), blackStroke); - + final double ftR = pW / 2; canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), 0, math.pi, false, blackStroke); for (int i = 0; i < 10; i++) { canvas.drawArc(Rect.fromCircle(center: Offset(basketX, pH), radius: ftR), math.pi + (i * 2 * (math.pi / 20)), math.pi / 20, false, blackStroke); } - + canvas.drawLine(Offset(basketX - pW / 2, pH), Offset(sXL, sYL), blackStroke); canvas.drawLine(Offset(basketX + pW / 2, pH), Offset(sXR, sYR), blackStroke); canvas.drawArc(Rect.fromCircle(center: Offset(basketX, h), radius: w * 0.12), math.pi, math.pi, false, blackStroke); canvas.drawCircle(Offset(basketX, h * 0.12), w * 0.02, blackStroke); canvas.drawLine(Offset(basketX - w * 0.08, h * 0.12 - 5), Offset(basketX + w * 0.08, h * 0.12 - 5), blackStroke); } - + @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } +// ============================================================================== +// PLAY BY PLAY DIALOG +// ============================================================================== + class PlayByPlayDialog extends StatelessWidget { final PlacarController controller; const PlayByPlayDialog({super.key, required this.controller}); - + @override Widget build(BuildContext context) { return Dialog( @@ -1387,12 +1374,16 @@ class PlayByPlayDialog extends StatelessWidget { } } +// ============================================================================== +// BOX SCORE DIALOG +// ============================================================================== + class BoxScoreDialog extends StatelessWidget { final PlacarController controller; final double sf; - + const BoxScoreDialog({super.key, required this.controller, required this.sf}); - + @override Widget build(BuildContext context) { return AnimatedBuilder( @@ -1400,10 +1391,7 @@ class BoxScoreDialog extends StatelessWidget { builder: (context, child) { return Dialog( backgroundColor: AppTheme.placarDarkSurface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12 * sf), - side: BorderSide(color: Colors.white24, width: 1 * sf), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12 * sf), side: BorderSide(color: Colors.white24, width: 1 * sf)), insetPadding: EdgeInsets.all(8 * sf), clipBehavior: Clip.antiAlias, child: SizedBox( @@ -1419,12 +1407,7 @@ class BoxScoreDialog extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("BOX SCORE", style: TextStyle(color: Colors.white, fontSize: 20 * sf, fontWeight: FontWeight.bold)), - IconButton( - icon: Icon(Icons.close, color: Colors.white, size: 24 * sf), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () => Navigator.pop(context), - ) + IconButton(icon: Icon(Icons.close, color: Colors.white, size: 24 * sf), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () => Navigator.pop(context)) ], ), ), @@ -1467,7 +1450,7 @@ class BoxScoreDialog extends StatelessWidget { }, ); } - + Widget _buildStatsTable(List teamPlayers, PlacarController ctrl, double sf) { return LayoutBuilder(builder: (context, constraints) { return SingleChildScrollView( @@ -1514,7 +1497,7 @@ class BoxScoreDialog extends StatelessWidget { final timeStr = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; final rebs = s['orb']! + s['drb']!; final fgText = "${s['fgm']}/${s['fga']}"; - + return DataRow(cells: [ DataCell(Text(name, style: const TextStyle(fontWeight: FontWeight.bold))), DataCell(Text(timeStr, style: const TextStyle(color: Colors.white70))), @@ -1529,7 +1512,7 @@ class BoxScoreDialog extends StatelessWidget { DataCell(Text((s['il'] ?? 0).toString(), style: const TextStyle(color: Colors.lightBlue))), DataCell(Text((s['li'] ?? 0).toString(), style: const TextStyle(color: Colors.orangeAccent))), DataCell(Text((s['pa'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), - DataCell(Text((s['tres_seg'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), // CORRIGIDO PARA MOSTRAR OS 3 SEG NO BOX SCORE + DataCell(Text((s['tres_seg'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), DataCell(Text((s['dr'] ?? 0).toString(), style: const TextStyle(color: Colors.redAccent))), DataCell(Text(fgText, style: const TextStyle(color: Colors.white54))), ]); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a5003c3..520b0aa 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import app_links import file_selector_macos import path_provider_foundation import printing +import share_plus import shared_preferences_foundation import sqflite_darwin import url_launcher_macos @@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 0daf6be..f3e2aba 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: archive - sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "4.0.9" + version: "3.6.1" async: dependency: transitive description: @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -177,6 +185,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" + excel: + dependency: "direct main" + description: + name: excel + sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780" + url: "https://pub.dev" + source: hosted + version: "4.0.6" fake_async: dependency: transitive description: @@ -189,10 +213,18 @@ packages: dependency: transitive description: name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" file: dependency: transitive description: @@ -288,6 +320,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" gotrue: dependency: transitive description: @@ -304,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + url: "https://pub.dev" + source: hosted + version: "1.0.3" http: dependency: transitive description: @@ -324,10 +372,10 @@ packages: dependency: transitive description: name: image - sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.3.0" image_cropper: dependency: "direct main" description: @@ -496,6 +544,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" nested: dependency: transitive description: @@ -624,14 +680,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - posix: - dependency: transitive - description: - name: posix - sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" - url: "https://pub.dev" - source: hosted - version: "6.5.0" postgrest: dependency: transitive description: @@ -656,6 +704,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" qr: dependency: transitive description: @@ -672,6 +728,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" retry: dependency: transitive description: @@ -688,6 +752,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: a857d8b1479250aff6b57a51b2c02d31ca05848d441817c43f1640c885c286c0 + url: "https://pub.dev" + source: hosted + version: "13.1.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "7f7ae28cf400d13f811e297ff37742dba83b79e0a6f5dce14eec0248274e6ce9" + url: "https://pub.dev" + source: hosted + version: "7.1.0" shared_preferences: dependency: "direct main" description: @@ -997,6 +1077,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: ba7d5750e3441caa1bbe31d9e516348fcf8dfcb32aa29ef87a844a59f4d1f1d0 + url: "https://pub.dev" + source: hosted + version: "6.1.0" xdg_directories: dependency: transitive description: @@ -1013,6 +1101,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" yet_another_json_isolate: dependency: transitive description: @@ -1023,4 +1119,4 @@ packages: version: "2.1.0" sdks: dart: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + flutter: ">=3.38.1" diff --git a/pubspec.yaml b/pubspec.yaml index 8ded917..cc30f3c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: shared_preferences: ^2.5.4 printing: ^5.14.3 pdf: ^3.12.0 + excel: ^4.0.6 + share_plus: ^13.1.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4c1d908..e328984 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); PrintingPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PrintingPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 5a8cfd6..7db787a 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links file_selector_windows printing + share_plus url_launcher_windows )