pdf e exel
This commit is contained in:
111
lib/controllers/active_team.dart
Normal file
111
lib/controllers/active_team.dart
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
class ActiveTeam {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String? logo;
|
||||||
|
final int wins;
|
||||||
|
final int losses;
|
||||||
|
final int draws;
|
||||||
|
|
||||||
|
ActiveTeam({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
this.logo,
|
||||||
|
this.wins = 0,
|
||||||
|
this.losses = 0,
|
||||||
|
this.draws = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🟢 A MÁGICA: Esta variável avisa a Home e a StatusPage ao mesmo tempo quando a equipa muda!
|
||||||
|
final ValueNotifier<ActiveTeam?> globalActiveTeam = ValueNotifier(null);
|
||||||
|
|
||||||
|
// 🟢 FUNÇÃO PARA CARREGAR A EQUIPA AO ABRIR A APP (Lê da Memória e do Supabase)
|
||||||
|
Future<void> loadGlobalTeam() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final savedId = prefs.getString('last_team_id');
|
||||||
|
|
||||||
|
// 1. Carrega rápido da memória (para não piscar o ecrã)
|
||||||
|
if (savedId != null) {
|
||||||
|
globalActiveTeam.value = ActiveTeam(
|
||||||
|
id: savedId,
|
||||||
|
name: prefs.getString('last_team_name') ?? "Selecionar Equipa",
|
||||||
|
logo: prefs.getString('last_team_logo'),
|
||||||
|
wins: prefs.getInt('last_team_wins') ?? 0,
|
||||||
|
losses: prefs.getInt('last_team_losses') ?? 0,
|
||||||
|
draws: prefs.getInt('last_team_draws') ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Vai confirmar no Supabase se entraste com esta conta noutro telemóvel!
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final profile = await supabase.from('profiles').select('selected_team_id').eq('id', userId).maybeSingle();
|
||||||
|
if (profile != null && profile['selected_team_id'] != null) {
|
||||||
|
final dbTeamId = profile['selected_team_id'].toString();
|
||||||
|
final teamData = await supabase.from('teams').select().eq('id', dbTeamId).maybeSingle();
|
||||||
|
|
||||||
|
if (teamData != null) {
|
||||||
|
final newTeam = ActiveTeam(
|
||||||
|
id: teamData['id'].toString(),
|
||||||
|
name: teamData['name'] ?? 'Desconhecido',
|
||||||
|
logo: teamData['image_url'],
|
||||||
|
wins: int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0,
|
||||||
|
losses: int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0,
|
||||||
|
draws: int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0,
|
||||||
|
);
|
||||||
|
globalActiveTeam.value = newTeam;
|
||||||
|
|
||||||
|
// Atualiza a memória do telemóvel para a próxima vez ser rápido
|
||||||
|
await prefs.setString('last_team_id', newTeam.id);
|
||||||
|
await prefs.setString('last_team_name', newTeam.name);
|
||||||
|
if (newTeam.logo != null && newTeam.logo!.isNotEmpty) {
|
||||||
|
await prefs.setString('last_team_logo', newTeam.logo!);
|
||||||
|
}
|
||||||
|
await prefs.setInt('last_team_wins', newTeam.wins);
|
||||||
|
await prefs.setInt('last_team_losses', newTeam.losses);
|
||||||
|
await prefs.setInt('last_team_draws', newTeam.draws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao carregar equipa do Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🟢 FUNÇÃO PARA GUARDAR A EQUIPA (Na Memória e no Supabase)
|
||||||
|
Future<void> saveGlobalTeam(ActiveTeam team) async {
|
||||||
|
globalActiveTeam.value = team; // Atualiza a app inteira!
|
||||||
|
|
||||||
|
// 1. Guarda no telemóvel
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString('last_team_id', team.id);
|
||||||
|
await prefs.setString('last_team_name', team.name);
|
||||||
|
if (team.logo != null && team.logo!.isNotEmpty) {
|
||||||
|
await prefs.setString('last_team_logo', team.logo!);
|
||||||
|
} else {
|
||||||
|
await prefs.remove('last_team_logo');
|
||||||
|
}
|
||||||
|
await prefs.setInt('last_team_wins', team.wins);
|
||||||
|
await prefs.setInt('last_team_losses', team.losses);
|
||||||
|
await prefs.setInt('last_team_draws', team.draws);
|
||||||
|
|
||||||
|
// 2. Guarda no Supabase!
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
final userId = supabase.auth.currentUser?.id;
|
||||||
|
if (userId != null) {
|
||||||
|
try {
|
||||||
|
await supabase.from('profiles').upsert({
|
||||||
|
'id': userId,
|
||||||
|
'selected_team_id': team.id,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao guardar equipa no Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,4 +94,4 @@ class GameController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
|
|||||||
375
lib/pages/excel_export_service.dart
Normal file
375
lib/pages/excel_export_service.dart
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:excel/excel.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart' hide Border, BorderStyle;
|
||||||
|
|
||||||
|
class ExcelExportService {
|
||||||
|
static Future<void> generateAndPrintBoxScoreExcel({
|
||||||
|
required String gameId,
|
||||||
|
required String myTeam,
|
||||||
|
required String opponentTeam,
|
||||||
|
required String myScore,
|
||||||
|
required String opponentScore,
|
||||||
|
required String season,
|
||||||
|
required String targetTeam,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
// ── 1. DADOS DO JOGO ───────────────────────────────────────────────────
|
||||||
|
final gameData = await supabase.from('games').select().eq('id', gameId).maybeSingle();
|
||||||
|
String dateStr = "---";
|
||||||
|
if (gameData != null && gameData['game_date'] != null) {
|
||||||
|
String rawDate = gameData['game_date'].toString();
|
||||||
|
dateStr = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. ESTATÍSTICAS DOS JOGADORES ──────────────────────────────────────
|
||||||
|
final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId);
|
||||||
|
if (statsResponse.isEmpty) return;
|
||||||
|
|
||||||
|
// ── 3. NOMES E NÚMEROS DAS EQUIPAS E JOGADORES ───────────────────────
|
||||||
|
final membersResponse = await supabase.from('members').select('id, name, number');
|
||||||
|
final Map<String, Map<String, dynamic>> memberInfo = {
|
||||||
|
for (var m in membersResponse) m['id'].toString(): m
|
||||||
|
};
|
||||||
|
|
||||||
|
final teamsResponse = await supabase.from('teams').select('id, name');
|
||||||
|
final Map<String, String> teamNames = {
|
||||||
|
for (var t in teamsResponse) t['id'].toString(): t['name'].toString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 4. CONFIGURAÇÃO DO EXCEL ───────────────────────────────────────────
|
||||||
|
var excel = Excel.createExcel();
|
||||||
|
String sheetName = 'Estatísticas';
|
||||||
|
Sheet sheet = excel[sheetName];
|
||||||
|
excel.setDefaultSheet(sheetName);
|
||||||
|
if (excel.tables.keys.contains('Sheet1')) excel.delete('Sheet1');
|
||||||
|
|
||||||
|
// ── ESTILOS E CORES PREMIUM ───────────────────────────────────────────
|
||||||
|
final corPrincipal = ExcelColor.fromHexString('#A00000'); // Vermelho escuro
|
||||||
|
final corFundoCinza = ExcelColor.fromHexString('#F5F5F5');
|
||||||
|
final corFundoCinzaEscuro = ExcelColor.fromHexString('#E0E0E0');
|
||||||
|
final cor2P = ExcelColor.fromHexString('#E3F2FD'); // Azul claro
|
||||||
|
final cor3P = ExcelColor.fromHexString('#E8F5E9'); // Verde claro
|
||||||
|
final corGlobal = ExcelColor.fromHexString('#FFF9C4');// Amarelo claro
|
||||||
|
final corLL = ExcelColor.fromHexString('#FFF3E0'); // Laranja claro
|
||||||
|
final corReb = ExcelColor.fromHexString('#F3E5F5'); // Roxo claro
|
||||||
|
final borderGrey = ExcelColor.fromHexString('#BDBDBD');
|
||||||
|
|
||||||
|
CellStyle styleTituloJogo = CellStyle(bold: true, fontSize: 16);
|
||||||
|
CellStyle styleNomeEquipa = CellStyle(bold: true, fontSize: 14, fontColorHex: ExcelColor.white, backgroundColorHex: corPrincipal, horizontalAlign: HorizontalAlign.Center, verticalAlign: VerticalAlign.Center);
|
||||||
|
CellStyle styleTituloSecundario = CellStyle(bold: true, fontSize: 12, fontColorHex: ExcelColor.black, backgroundColorHex: corFundoCinzaEscuro, horizontalAlign: HorizontalAlign.Center, verticalAlign: VerticalAlign.Center);
|
||||||
|
|
||||||
|
CellStyle styleGrelha(ExcelColor bgCol, {bool isBold = false}) {
|
||||||
|
return CellStyle(
|
||||||
|
bold: isBold, backgroundColorHex: bgCol,
|
||||||
|
horizontalAlign: HorizontalAlign.Center, verticalAlign: VerticalAlign.Center,
|
||||||
|
leftBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
|
||||||
|
rightBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
|
||||||
|
topBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
|
||||||
|
bottomBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final styleGeral = styleGrelha(ExcelColor.white);
|
||||||
|
final styleGeralBold = styleGrelha(ExcelColor.white, isBold: true);
|
||||||
|
final styleNome = CellStyle(horizontalAlign: HorizontalAlign.Left, verticalAlign: VerticalAlign.Center, leftBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey), rightBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey), topBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey), bottomBorder: Border(borderStyle: BorderStyle.Thin, borderColorHex: borderGrey));
|
||||||
|
|
||||||
|
// ── CABEÇALHO DO JOGO ────────────────────────────────────────────────
|
||||||
|
sheet.cell(CellIndex.indexByString("A1")).value = TextCellValue("JOGO:");
|
||||||
|
sheet.cell(CellIndex.indexByString("A1")).cellStyle = CellStyle(bold: true);
|
||||||
|
sheet.cell(CellIndex.indexByString("B1")).value = TextCellValue("$myTeam vs $opponentTeam");
|
||||||
|
sheet.cell(CellIndex.indexByString("B1")).cellStyle = styleTituloJogo;
|
||||||
|
|
||||||
|
sheet.cell(CellIndex.indexByString("A2")).value = TextCellValue("COMPETIÇÃO:");
|
||||||
|
sheet.cell(CellIndex.indexByString("A2")).cellStyle = CellStyle(bold: true);
|
||||||
|
sheet.cell(CellIndex.indexByString("B2")).value = TextCellValue(season);
|
||||||
|
|
||||||
|
sheet.cell(CellIndex.indexByString("A3")).value = TextCellValue("DATA:");
|
||||||
|
sheet.cell(CellIndex.indexByString("A3")).cellStyle = CellStyle(bold: true);
|
||||||
|
sheet.cell(CellIndex.indexByString("B3")).value = TextCellValue(dateStr);
|
||||||
|
|
||||||
|
sheet.cell(CellIndex.indexByString("A4")).value = TextCellValue("RESULTADO:");
|
||||||
|
sheet.cell(CellIndex.indexByString("A4")).cellStyle = CellStyle(bold: true);
|
||||||
|
sheet.cell(CellIndex.indexByString("B4")).value = TextCellValue("$myScore - $opponentScore");
|
||||||
|
sheet.cell(CellIndex.indexByString("B4")).cellStyle = CellStyle(bold: true, fontColorHex: corPrincipal);
|
||||||
|
|
||||||
|
// ── TOTAIS POR PERÍODO (NOVA SECÇÃO) ─────────────────────────────────
|
||||||
|
sheet.cell(CellIndex.indexByString("A6")).value = TextCellValue("PONTUAÇÃO POR PERÍODO");
|
||||||
|
sheet.cell(CellIndex.indexByString("A6")).cellStyle = CellStyle(bold: true, fontColorHex: corPrincipal);
|
||||||
|
|
||||||
|
List<String> periodHeaders = ["EQUIPA", "1º Q", "2º Q", "3º Q", "4º Q", "TOTAL"];
|
||||||
|
for (int i = 0; i < periodHeaders.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 6));
|
||||||
|
cell.value = TextCellValue(periodHeaders[i]);
|
||||||
|
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linha Minha Equipa
|
||||||
|
List<dynamic> myRow = [
|
||||||
|
myTeam,
|
||||||
|
gameData?['my_q1']?.toString() ?? '-',
|
||||||
|
gameData?['my_q2']?.toString() ?? '-',
|
||||||
|
gameData?['my_q3']?.toString() ?? '-',
|
||||||
|
gameData?['my_q4']?.toString() ?? '-',
|
||||||
|
myScore
|
||||||
|
];
|
||||||
|
for (int i = 0; i < myRow.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 7));
|
||||||
|
cell.value = TextCellValue(myRow[i].toString());
|
||||||
|
cell.cellStyle = i == 0 ? styleNome : styleGeralBold;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linha Adversário
|
||||||
|
List<dynamic> oppRow = [
|
||||||
|
opponentTeam,
|
||||||
|
gameData?['opp_q1']?.toString() ?? '-',
|
||||||
|
gameData?['opp_q2']?.toString() ?? '-',
|
||||||
|
gameData?['opp_q3']?.toString() ?? '-',
|
||||||
|
gameData?['opp_q4']?.toString() ?? '-',
|
||||||
|
opponentScore
|
||||||
|
];
|
||||||
|
for (int i = 0; i < oppRow.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 8));
|
||||||
|
cell.value = TextCellValue(oppRow[i].toString());
|
||||||
|
cell.cellStyle = i == 0 ? styleNome : styleGeralBold;
|
||||||
|
}
|
||||||
|
|
||||||
|
int r = 11; // 👈 AS TABELAS PRINCIPAIS AGORA COMEÇAM MAIS ABAIXO (Linha 12 no Excel)
|
||||||
|
|
||||||
|
// Agrupar estatísticas por equipa
|
||||||
|
Map<String, List<dynamic>> statsByTeam = {};
|
||||||
|
for(var s in statsResponse) {
|
||||||
|
String tId = s['team_id'].toString();
|
||||||
|
statsByTeam.putIfAbsent(tId, () => []).add(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CONSTRUÇÃO DAS TABELAS DE CADA EQUIPA ────────────────────────────
|
||||||
|
for (var entry in statsByTeam.entries) {
|
||||||
|
String tId = entry.key;
|
||||||
|
List<dynamic> tStats = entry.value;
|
||||||
|
String tName = teamNames[tId] ?? "Equipa $tId";
|
||||||
|
|
||||||
|
if (targetTeam != 'Ambas' && tName != targetTeam) continue;
|
||||||
|
|
||||||
|
tStats.sort((a, b) {
|
||||||
|
var mInfoA = memberInfo[a['member_id'].toString()];
|
||||||
|
var mInfoB = memberInfo[b['member_id'].toString()];
|
||||||
|
int numA = int.tryParse(mInfoA?['number']?.toString() ?? '0') ?? 0;
|
||||||
|
int numB = int.tryParse(mInfoB?['number']?.toString() ?? '0') ?? 0;
|
||||||
|
return numA.compareTo(numB);
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> processedPlayers = [];
|
||||||
|
int tMin=0, tPts=0, t2m=0, t2a=0, t3m=0, t3a=0, tFgm=0, tFga=0, tftm=0, tfta=0;
|
||||||
|
int torb=0, tdrb=0, tStl=0, tAst=0, tTov=0, tBlk=0, tFls=0;
|
||||||
|
int tSo=0, tIl=0, tLi=0, tPa=0, tTresS=0, tDr=0;
|
||||||
|
|
||||||
|
for(var stat in tStats) {
|
||||||
|
var mInfo = memberInfo[stat['member_id'].toString()];
|
||||||
|
String pNum = mInfo != null ? (mInfo['number']?.toString() ?? "-") : "-";
|
||||||
|
String pName = mInfo != null ? (mInfo['name']?.toString() ?? "Desconhecido") : "Desconhecido";
|
||||||
|
|
||||||
|
int minSecs = stat['minutos_jogados'] ?? 0;
|
||||||
|
int pts = stat['pts'] ?? 0;
|
||||||
|
int p2m = stat['p2m'] ?? 0; int p2a = stat['p2a'] ?? 0;
|
||||||
|
int p3m = stat['p3m'] ?? 0; int p3a = stat['p3a'] ?? 0;
|
||||||
|
int fgm = stat['fgm'] ?? 0; int fga = stat['fga'] ?? 0;
|
||||||
|
int ftm = stat['ftm'] ?? 0; int fta = stat['fta'] ?? 0;
|
||||||
|
int orb = stat['orb'] ?? 0; int drb = stat['drb'] ?? 0; int tr = orb + drb;
|
||||||
|
int stl = stat['stl'] ?? 0; int ast = stat['ast'] ?? 0;
|
||||||
|
int tov = stat['tov'] ?? 0; int blk = stat['blk'] ?? 0; int fls = stat['fls'] ?? 0;
|
||||||
|
int so = stat['so'] ?? 0; int il = stat['il'] ?? 0; int li = stat['li'] ?? 0;
|
||||||
|
int pa = stat['pa'] ?? 0; int tresS = stat['tres_seg'] ?? 0; int dr = stat['dr'] ?? 0;
|
||||||
|
|
||||||
|
tMin+=minSecs; tPts+=pts; t2m+=p2m; t2a+=p2a; t3m+=p3m; t3a+=p3a;
|
||||||
|
tFgm+=fgm; tFga+=fga; tftm+=ftm; tfta+=fta; torb+=orb; tdrb+=drb;
|
||||||
|
tStl+=stl; tAst+=ast; tTov+=tov; tBlk+=blk; tFls+=fls;
|
||||||
|
tSo+=so; tIl+=il; tLi+=li; tPa+=pa; tTresS+=tresS; tDr+=dr;
|
||||||
|
|
||||||
|
processedPlayers.add({
|
||||||
|
'num': pNum, 'name': pName, 'minSecs': minSecs, 'pts': pts,
|
||||||
|
'p2m': p2m, 'p2a': p2a, 'p3m': p3m, 'p3a': p3a, 'fgm': fgm, 'fga': fga,
|
||||||
|
'ftm': ftm, 'fta': fta, 'orb': orb, 'drb': drb, 'tr': tr,
|
||||||
|
'stl': stl, 'ast': ast, 'tov': tov, 'blk': blk, 'fls': fls,
|
||||||
|
'so': so, 'il': il, 'li': li, 'pa': pa, '3s': tresS, 'dr': dr
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TABELA 1: LANÇAMENTOS E RESSALTOS
|
||||||
|
var teamStart = CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r);
|
||||||
|
var teamEnd = CellIndex.indexByColumnRow(columnIndex: 18, rowIndex: r);
|
||||||
|
sheet.merge(teamStart, teamEnd, customValue: TextCellValue("ESTATÍSTICAS DA EQUIPA: ${tName.toUpperCase()} (Lançamentos e Ressaltos)"));
|
||||||
|
for(int i=0; i<=18; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleNomeEquipa;
|
||||||
|
r++;
|
||||||
|
|
||||||
|
void criarCategoria(int colStart, int colEnd, String texto, CellStyle estilo) {
|
||||||
|
sheet.merge(CellIndex.indexByColumnRow(columnIndex: colStart, rowIndex: r), CellIndex.indexByColumnRow(columnIndex: colEnd, rowIndex: r), customValue: TextCellValue(texto));
|
||||||
|
for(int i=colStart; i<=colEnd; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = estilo;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i=0; i<=3; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleGrelha(corFundoCinza);
|
||||||
|
criarCategoria(4, 6, "2 PONTOS", styleGrelha(cor2P, isBold: true));
|
||||||
|
criarCategoria(7, 9, "3 PONTOS", styleGrelha(cor3P, isBold: true));
|
||||||
|
criarCategoria(10, 12, "GLOBAL", styleGrelha(corGlobal, isBold: true));
|
||||||
|
criarCategoria(13, 15, "L. LIVRES", styleGrelha(corLL, isBold: true));
|
||||||
|
criarCategoria(16, 18, "RESSALTOS", styleGrelha(corReb, isBold: true));
|
||||||
|
r++;
|
||||||
|
|
||||||
|
List<String> colsT1 = ["Nº", "NOME", "MIN", "PTS", "C", "T", "%", "C", "T", "%", "C", "T", "%", "C", "T", "%", "RO", "RD", "TR"];
|
||||||
|
for(int i = 0; i < colsT1.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = TextCellValue(colsT1[i]);
|
||||||
|
|
||||||
|
if (i >= 4 && i <= 6) cell.cellStyle = styleGrelha(cor2P, isBold: true);
|
||||||
|
else if (i >= 7 && i <= 9) cell.cellStyle = styleGrelha(cor3P, isBold: true);
|
||||||
|
else if (i >= 10 && i <= 12) cell.cellStyle = styleGrelha(corGlobal, isBold: true);
|
||||||
|
else if (i >= 13 && i <= 15) cell.cellStyle = styleGrelha(corLL, isBold: true);
|
||||||
|
else if (i >= 16 && i <= 18) cell.cellStyle = styleGrelha(corReb, isBold: true);
|
||||||
|
else cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
}
|
||||||
|
r++;
|
||||||
|
|
||||||
|
for(var p in processedPlayers) {
|
||||||
|
String minStr = '${p['minSecs'] ~/ 60}:${(p['minSecs'] % 60).toString().padLeft(2, '0')}';
|
||||||
|
String p2Pct = p['p2a'] > 0 ? '${((p['p2m'] / p['p2a']) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String p3Pct = p['p3a'] > 0 ? '${((p['p3m'] / p['p3a']) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String fgPct = p['fga'] > 0 ? '${((p['fgm'] / p['fga']) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String ftPct = p['fta'] > 0 ? '${((p['ftm'] / p['fta']) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
|
||||||
|
List<CellValue> rowData = [
|
||||||
|
TextCellValue(p['num']), TextCellValue(p['name']), TextCellValue(minStr), IntCellValue(p['pts']),
|
||||||
|
IntCellValue(p['p2m']), IntCellValue(p['p2a']), TextCellValue(p2Pct),
|
||||||
|
IntCellValue(p['p3m']), IntCellValue(p['p3a']), TextCellValue(p3Pct),
|
||||||
|
IntCellValue(p['fgm']), IntCellValue(p['fga']), TextCellValue(fgPct),
|
||||||
|
IntCellValue(p['ftm']), IntCellValue(p['fta']), TextCellValue(ftPct),
|
||||||
|
IntCellValue(p['orb']), IntCellValue(p['drb']), IntCellValue(p['tr'])
|
||||||
|
];
|
||||||
|
|
||||||
|
for(int i = 0; i < rowData.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = rowData[i];
|
||||||
|
if (i == 1) cell.cellStyle = styleNome;
|
||||||
|
else if (i == 3) cell.cellStyle = styleGeralBold;
|
||||||
|
else cell.cellStyle = styleGeral;
|
||||||
|
}
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
|
||||||
|
String t2Pct = t2a > 0 ? '${((t2m / t2a) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String t3Pct = t3a > 0 ? '${((t3m / t3a) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String tFgPct = tFga > 0 ? '${((tFgm / tFga) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String tftPct = tfta > 0 ? '${((tftm / tfta) * 100).toStringAsFixed(0)}%' : '-';
|
||||||
|
String tMinStr = '${tMin ~/ 60}:${(tMin % 60).toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
List<CellValue> totalRowT1 = [
|
||||||
|
TextCellValue(""), TextCellValue("TOTAL EQUIPA"), TextCellValue(tMinStr), IntCellValue(tPts),
|
||||||
|
IntCellValue(t2m), IntCellValue(t2a), TextCellValue(t2Pct),
|
||||||
|
IntCellValue(t3m), IntCellValue(t3a), TextCellValue(t3Pct),
|
||||||
|
IntCellValue(tFgm), IntCellValue(tFga), TextCellValue(tFgPct),
|
||||||
|
IntCellValue(tftm), IntCellValue(tfta), TextCellValue(tftPct),
|
||||||
|
IntCellValue(torb), IntCellValue(tdrb), IntCellValue(torb + tdrb)
|
||||||
|
];
|
||||||
|
|
||||||
|
for(int i = 0; i < totalRowT1.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = totalRowT1[i];
|
||||||
|
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
if (i >= 4 && i <= 6) cell.cellStyle = styleGrelha(cor2P, isBold: true);
|
||||||
|
else if (i >= 7 && i <= 9) cell.cellStyle = styleGrelha(cor3P, isBold: true);
|
||||||
|
else if (i >= 10 && i <= 12) cell.cellStyle = styleGrelha(corGlobal, isBold: true);
|
||||||
|
else if (i >= 13 && i <= 15) cell.cellStyle = styleGrelha(corLL, isBold: true);
|
||||||
|
else if (i >= 16 && i <= 18) cell.cellStyle = styleGrelha(corReb, isBold: true);
|
||||||
|
}
|
||||||
|
r += 3;
|
||||||
|
|
||||||
|
// TABELA 2: OUTRAS ESTATÍSTICAS
|
||||||
|
var secStart = CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r);
|
||||||
|
var secEnd = CellIndex.indexByColumnRow(columnIndex: 12, rowIndex: r);
|
||||||
|
sheet.merge(secStart, secEnd, customValue: TextCellValue("OUTRAS ESTATÍSTICAS: ${tName.toUpperCase()}"));
|
||||||
|
for(int i=0; i<=12; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleTituloSecundario;
|
||||||
|
r++;
|
||||||
|
|
||||||
|
List<String> colsT2 = ["Nº", "NOME", "BR", "AS", "BP", "BLK", "FLS", "SO", "IL", "LI", "PA", "3S", "DR"];
|
||||||
|
for(int i = 0; i < colsT2.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = TextCellValue(colsT2[i]);
|
||||||
|
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
}
|
||||||
|
r++;
|
||||||
|
|
||||||
|
for(var p in processedPlayers) {
|
||||||
|
List<CellValue> rowData2 = [
|
||||||
|
TextCellValue(p['num']), TextCellValue(p['name']),
|
||||||
|
IntCellValue(p['stl']), IntCellValue(p['ast']), IntCellValue(p['tov']),
|
||||||
|
IntCellValue(p['blk']), IntCellValue(p['fls']), IntCellValue(p['so']),
|
||||||
|
IntCellValue(p['il']), IntCellValue(p['li']), IntCellValue(p['pa']),
|
||||||
|
IntCellValue(p['3s']), IntCellValue(p['dr'])
|
||||||
|
];
|
||||||
|
for(int i = 0; i < rowData2.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = rowData2[i];
|
||||||
|
cell.cellStyle = (i == 1) ? styleNome : styleGeral;
|
||||||
|
}
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CellValue> totalRowT2 = [
|
||||||
|
TextCellValue(""), TextCellValue("TOTAL EQUIPA"),
|
||||||
|
IntCellValue(tStl), IntCellValue(tAst), IntCellValue(tTov), IntCellValue(tBlk), IntCellValue(tFls),
|
||||||
|
IntCellValue(tSo), IntCellValue(tIl), IntCellValue(tLi), IntCellValue(tPa), IntCellValue(tTresS), IntCellValue(tDr)
|
||||||
|
];
|
||||||
|
for(int i = 0; i < totalRowT2.length; i++) {
|
||||||
|
var cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r));
|
||||||
|
cell.value = totalRowT2[i];
|
||||||
|
cell.cellStyle = styleGrelha(corFundoCinza, isBold: true);
|
||||||
|
}
|
||||||
|
r += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DESTAQUES DO JOGO ───────────────────────────────────────
|
||||||
|
if (gameData != null) {
|
||||||
|
var startD = CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r);
|
||||||
|
var endD = CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: r);
|
||||||
|
sheet.merge(startD, endD, customValue: TextCellValue("DESTAQUES DO JOGO"));
|
||||||
|
for(int i=0; i<=3; i++) sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: r)).cellStyle = styleNomeEquipa;
|
||||||
|
r++;
|
||||||
|
|
||||||
|
void adicionarDestaque(String titulo, String valor) {
|
||||||
|
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r)).value = TextCellValue(titulo);
|
||||||
|
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: r)).cellStyle = CellStyle(bold: true);
|
||||||
|
var sV = CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: r);
|
||||||
|
var eV = CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: r);
|
||||||
|
sheet.merge(sV, eV, customValue: TextCellValue(valor));
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
|
||||||
|
adicionarDestaque("Melhor Marcador:", gameData['top_pts_name'] ?? '---');
|
||||||
|
adicionarDestaque("Melhor Ressaltador:", gameData['top_rbs_name'] ?? '---');
|
||||||
|
adicionarDestaque("Melhor Passador:", gameData['top_ast_name'] ?? '---');
|
||||||
|
adicionarDestaque("MVP da Partida:", gameData['mvp_name'] ?? '---');
|
||||||
|
}
|
||||||
|
|
||||||
|
sheet.setColumnWidth(0, 18.0);
|
||||||
|
sheet.setColumnWidth(1, 26.0);
|
||||||
|
sheet.setColumnWidth(2, 8.0);
|
||||||
|
sheet.setColumnWidth(3, 6.0);
|
||||||
|
for(int i=4; i<=18; i++) sheet.setColumnWidth(i, 5.5);
|
||||||
|
|
||||||
|
var fileBytes = excel.save();
|
||||||
|
if (fileBytes != null) {
|
||||||
|
final directory = await getTemporaryDirectory();
|
||||||
|
String safeName = targetTeam == 'Ambas' ? '${myTeam}_vs_${opponentTeam}'.replaceAll(' ', '_') : targetTeam.replaceAll(' ', '_');
|
||||||
|
final filePath = '${directory.path}/BoxScore_$safeName.xlsx';
|
||||||
|
|
||||||
|
File(filePath)..createSync(recursive: true)..writeAsBytesSync(fileBytes);
|
||||||
|
await Share.shareXFiles([XFile(filePath)], text: 'Estatísticas do Jogo: $myTeam vs $opponentTeam');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Erro ao gerar Excel: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import '../models/game_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:playmaker/pages/PlacarPage.dart';
|
import 'package:playmaker/pages/PlacarPage.dart';
|
||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../controllers/game_controller.dart';
|
import '../controllers/game_controller.dart';
|
||||||
import '../models/game_model.dart';
|
|
||||||
import '../utils/size_extension.dart';
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
import 'pdf_export_service.dart';
|
import 'pdf_export_service.dart';
|
||||||
|
import 'excel_export_service.dart';
|
||||||
|
|
||||||
class GameResultCard extends StatelessWidget {
|
class GameResultCard extends StatelessWidget {
|
||||||
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
|
final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season;
|
||||||
@@ -21,6 +23,67 @@ class GameResultCard extends StatelessWidget {
|
|||||||
this.myTeamLogo, this.opponentTeamLogo, required this.sf, required this.onDelete,
|
this.myTeamLogo, this.opponentTeamLogo, required this.sf, required this.onDelete,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void _showTeamSelectionDialog(BuildContext context, String format) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)),
|
||||||
|
title: Text('Gerar ${format.toUpperCase()}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
content: Text('De qual equipa pretende exportar as estatísticas?', style: TextStyle(fontSize: 14 * sf, color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_exportDocument(context, format, myTeam);
|
||||||
|
},
|
||||||
|
child: Text(myTeam, style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf))
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_exportDocument(context, format, opponentTeam);
|
||||||
|
},
|
||||||
|
child: Text(opponentTeam, style: TextStyle(color: AppTheme.primaryRed, fontSize: 14 * sf))
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
_exportDocument(context, format, 'Ambas');
|
||||||
|
},
|
||||||
|
child: Text('Ambas', style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * sf))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _exportDocument(BuildContext context, String format, String targetTeam) async {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('A gerar ${format.toUpperCase()}...'), duration: const Duration(seconds: 1)));
|
||||||
|
|
||||||
|
if (format == 'pdf') {
|
||||||
|
await PdfExportService.generateAndPrintBoxScore(
|
||||||
|
gameId: gameId,
|
||||||
|
myTeam: myTeam,
|
||||||
|
opponentTeam: opponentTeam,
|
||||||
|
myScore: myScore,
|
||||||
|
opponentScore: opponentScore,
|
||||||
|
season: season,
|
||||||
|
targetTeam: targetTeam,
|
||||||
|
);
|
||||||
|
} else if (format == 'excel') {
|
||||||
|
await ExcelExportService.generateAndPrintBoxScoreExcel(
|
||||||
|
gameId: gameId,
|
||||||
|
myTeam: myTeam,
|
||||||
|
opponentTeam: opponentTeam,
|
||||||
|
myScore: myScore,
|
||||||
|
opponentScore: opponentScore,
|
||||||
|
season: season,
|
||||||
|
targetTeam: targetTeam,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
|
final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface;
|
||||||
@@ -46,32 +109,71 @@ class GameResultCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 👇 MENU DOS 3 PONTOS (MAIS NÍTIDO E MODERNO)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: -10 * sf,
|
top: -12 * sf,
|
||||||
right: -10 * sf,
|
right: -12 * sf,
|
||||||
child: Row(
|
child: PopupMenuButton<String>(
|
||||||
children: [
|
icon: Icon(Icons.more_vert, color: Colors.grey.shade600, size: 26 * sf), // Ícone um pouco maior
|
||||||
IconButton(
|
splashRadius: 24 * sf,
|
||||||
icon: Icon(Icons.picture_as_pdf, color: AppTheme.primaryRed.withOpacity(0.8), size: 22 * sf),
|
elevation: 8, // Adiciona sombra para não se misturar com o fundo
|
||||||
splashRadius: 20 * sf,
|
shadowColor: Colors.black45,
|
||||||
tooltip: 'Gerar PDF',
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16 * sf)),
|
||||||
onPressed: () async {
|
color: Theme.of(context).colorScheme.surface,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('A gerar PDF...'), duration: Duration(seconds: 1)));
|
surfaceTintColor: Theme.of(context).colorScheme.surface, // Previne que o material 3 mude a cor
|
||||||
await PdfExportService.generateAndPrintBoxScore(
|
onSelected: (value) {
|
||||||
gameId: gameId,
|
if (value == 'pdf' || value == 'excel') {
|
||||||
myTeam: myTeam,
|
_showTeamSelectionDialog(context, value);
|
||||||
opponentTeam: opponentTeam,
|
} else if (value == 'delete') {
|
||||||
myScore: myScore,
|
_showDeleteConfirmation(context);
|
||||||
opponentScore: opponentScore,
|
}
|
||||||
season: season,
|
},
|
||||||
);
|
itemBuilder: (context) => [
|
||||||
},
|
PopupMenuItem(
|
||||||
|
value: 'pdf',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Ícone com fundo arredondado
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8 * sf),
|
||||||
|
decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.1), shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.picture_as_pdf, color: AppTheme.primaryRed, size: 20 * sf),
|
||||||
|
),
|
||||||
|
SizedBox(width: 14 * sf),
|
||||||
|
Text('Gerar PDF', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
PopupMenuItem(
|
||||||
icon: Icon(Icons.delete_outline, color: Colors.grey.shade400, size: 22 * sf),
|
value: 'excel',
|
||||||
splashRadius: 20 * sf,
|
child: Row(
|
||||||
tooltip: 'Eliminar Jogo',
|
children: [
|
||||||
onPressed: () => _showDeleteConfirmation(context),
|
// Ícone com fundo arredondado
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8 * sf),
|
||||||
|
decoration: BoxDecoration(color: Colors.green.shade600.withOpacity(0.1), shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.table_chart, color: Colors.green.shade600, size: 20 * sf),
|
||||||
|
),
|
||||||
|
SizedBox(width: 14 * sf),
|
||||||
|
Text('Gerar Excel', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PopupMenuDivider(height: 1),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Ícone com fundo arredondado
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8 * sf),
|
||||||
|
decoration: BoxDecoration(color: Colors.grey.shade500.withOpacity(0.1), shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.delete_outline, color: Colors.grey.shade700, size: 20 * sf),
|
||||||
|
),
|
||||||
|
SizedBox(width: 14 * sf),
|
||||||
|
Text('Eliminar Jogo', style: TextStyle(fontSize: 15 * sf, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
final TeamController _teamController = TeamController();
|
final TeamController _teamController = TeamController();
|
||||||
String? _selectedTeamId;
|
String? _selectedTeamId;
|
||||||
String _selectedTeamName = "Selecionar Equipa";
|
String _selectedTeamName = "Selecionar Equipa";
|
||||||
|
String? _selectedTeamLogo;
|
||||||
|
|
||||||
int _teamWins = 0;
|
int _teamWins = 0;
|
||||||
int _teamLosses = 0;
|
int _teamLosses = 0;
|
||||||
@@ -31,47 +32,113 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
String? _avatarUrl;
|
String? _avatarUrl;
|
||||||
bool _isMemoryLoaded = false; // A variável mágica que impede o "piscar" inicial
|
bool _isMemoryLoaded = false;
|
||||||
|
|
||||||
|
// A chave mágica para forçar a StatusPage a atualizar
|
||||||
|
String _statusKey = 'status_page_inicial';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadUserAvatar();
|
_loadUserAvatar();
|
||||||
|
_loadSelectedTeam();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSelectedTeam() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final savedId = prefs.getString('last_team_id');
|
||||||
|
|
||||||
|
if (savedId != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_selectedTeamId = savedId;
|
||||||
|
_selectedTeamName = prefs.getString('last_team_name') ?? "Selecionar Equipa";
|
||||||
|
_selectedTeamLogo = prefs.getString('last_team_logo');
|
||||||
|
_teamWins = prefs.getInt('last_team_wins') ?? 0;
|
||||||
|
_teamLosses = prefs.getInt('last_team_losses') ?? 0;
|
||||||
|
_teamDraws = prefs.getInt('last_team_draws') ?? 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
|
if (userId == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final profile = await _supabase.from('profiles').select('selected_team_id').eq('id', userId).maybeSingle();
|
||||||
|
|
||||||
|
if (profile != null && profile['selected_team_id'] != null) {
|
||||||
|
final dbTeamId = profile['selected_team_id'].toString();
|
||||||
|
final teamData = await _supabase.from('teams').select().eq('id', dbTeamId).maybeSingle();
|
||||||
|
|
||||||
|
if (teamData != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_selectedTeamId = teamData['id'].toString();
|
||||||
|
_selectedTeamName = teamData['name'] ?? 'Desconhecido';
|
||||||
|
_selectedTeamLogo = teamData['image_url'];
|
||||||
|
_teamWins = int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0;
|
||||||
|
_teamLosses = int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0;
|
||||||
|
_teamDraws = int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0;
|
||||||
|
});
|
||||||
|
await _saveToSharedPreferences();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao carregar equipa do Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveSelectedTeam() async {
|
||||||
|
await _saveToSharedPreferences();
|
||||||
|
|
||||||
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
|
if (userId != null && _selectedTeamId != null) {
|
||||||
|
try {
|
||||||
|
await _supabase.from('profiles').upsert({
|
||||||
|
'id': userId,
|
||||||
|
'selected_team_id': _selectedTeamId,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao guardar equipa no Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveToSharedPreferences() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (_selectedTeamId != null) {
|
||||||
|
await prefs.setString('last_team_id', _selectedTeamId!);
|
||||||
|
await prefs.setString('last_team_name', _selectedTeamName);
|
||||||
|
if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) {
|
||||||
|
await prefs.setString('last_team_logo', _selectedTeamLogo!);
|
||||||
|
} else {
|
||||||
|
await prefs.remove('last_team_logo');
|
||||||
|
}
|
||||||
|
await prefs.setInt('last_team_wins', _teamWins);
|
||||||
|
await prefs.setInt('last_team_losses', _teamLosses);
|
||||||
|
await prefs.setInt('last_team_draws', _teamDraws);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FUNÇÃO OTIMIZADA: Carrega da memória instantaneamente e atualiza em background
|
|
||||||
Future<void> _loadUserAvatar() async {
|
Future<void> _loadUserAvatar() async {
|
||||||
// 1. LÊ DA MEMÓRIA RÁPIDA PRIMEIRO
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final savedUrl = prefs.getString('meu_avatar_guardado');
|
final savedUrl = prefs.getString('meu_avatar_guardado');
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (savedUrl != null) _avatarUrl = savedUrl;
|
if (savedUrl != null) _avatarUrl = savedUrl;
|
||||||
_isMemoryLoaded = true; // Avisa o ecrã que a memória já respondeu!
|
_isMemoryLoaded = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. VAI AO SUPABASE VERIFICAR SE TROCASTE DE FOTO
|
|
||||||
final userId = _supabase.auth.currentUser?.id;
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
if (userId == null) return;
|
if (userId == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final data = await _supabase
|
final data = await _supabase.from('profiles').select('avatar_url').eq('id', userId).maybeSingle();
|
||||||
.from('profiles')
|
|
||||||
.select('avatar_url')
|
|
||||||
.eq('id', userId)
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (mounted && data != null && data['avatar_url'] != null) {
|
if (mounted && data != null && data['avatar_url'] != null) {
|
||||||
final urlDoSupabase = data['avatar_url'];
|
final urlDoSupabase = data['avatar_url'];
|
||||||
|
|
||||||
// Se a foto na base de dados for nova, ele guarda e atualiza!
|
|
||||||
if (urlDoSupabase != savedUrl) {
|
if (urlDoSupabase != savedUrl) {
|
||||||
await prefs.setString('meu_avatar_guardado', urlDoSupabase);
|
await prefs.setString('meu_avatar_guardado', urlDoSupabase);
|
||||||
setState(() {
|
setState(() { _avatarUrl = urlDoSupabase; });
|
||||||
_avatarUrl = urlDoSupabase;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -85,7 +152,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
_buildHomeContent(context),
|
_buildHomeContent(context),
|
||||||
const GamePage(),
|
const GamePage(),
|
||||||
const TeamsPage(),
|
const TeamsPage(),
|
||||||
const StatusPage(),
|
StatusPage(key: ValueKey(_statusKey)), // A StatusPage recarrega sempre que a chave muda!
|
||||||
];
|
];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -95,55 +162,37 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
backgroundColor: AppTheme.primaryRed,
|
backgroundColor: AppTheme.primaryRed,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
|
||||||
leading: Padding(
|
leading: Padding(
|
||||||
padding: EdgeInsets.all(10.0 * context.sf),
|
padding: EdgeInsets.all(10.0 * context.sf),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(100),
|
borderRadius: BorderRadius.circular(100),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await Navigator.push(
|
await Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen()));
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
|
||||||
);
|
|
||||||
_loadUserAvatar();
|
_loadUserAvatar();
|
||||||
},
|
},
|
||||||
// SÓ MOSTRA A IMAGEM OU O BONECO DEPOIS DE LER A MEMÓRIA
|
|
||||||
child: !_isMemoryLoaded
|
child: !_isMemoryLoaded
|
||||||
// Nos primeiros 0.05 segs, mostra só o círculo de fundo (sem boneco)
|
|
||||||
? CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2))
|
? CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2))
|
||||||
|
|
||||||
// Depois da memória responder:
|
|
||||||
: _avatarUrl != null && _avatarUrl!.isNotEmpty
|
: _avatarUrl != null && _avatarUrl!.isNotEmpty
|
||||||
? CachedNetworkImage(
|
? CachedNetworkImage(
|
||||||
imageUrl: _avatarUrl!,
|
imageUrl: _avatarUrl!,
|
||||||
fadeInDuration: Duration.zero, // Corta o atraso visual!
|
fadeInDuration: Duration.zero,
|
||||||
imageBuilder: (context, imageProvider) => CircleAvatar(
|
imageBuilder: (context, imageProvider) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), backgroundImage: imageProvider),
|
||||||
backgroundColor: Colors.white.withOpacity(0.2),
|
|
||||||
backgroundImage: imageProvider,
|
|
||||||
),
|
|
||||||
placeholder: (context, url) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)),
|
placeholder: (context, url) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)),
|
||||||
errorWidget: (context, url, error) => CircleAvatar(
|
errorWidget: (context, url, error) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf)),
|
||||||
backgroundColor: Colors.white.withOpacity(0.2),
|
|
||||||
child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
// Se não tiver foto nenhuma, aí sim mostra o boneco
|
: CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf)),
|
||||||
: CircleAvatar(
|
|
||||||
backgroundColor: Colors.white.withOpacity(0.2),
|
|
||||||
child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
body: IndexedStack(index: _selectedIndex, children: pages),
|
||||||
body: IndexedStack(
|
|
||||||
index: _selectedIndex,
|
|
||||||
children: pages,
|
|
||||||
),
|
|
||||||
|
|
||||||
bottomNavigationBar: NavigationBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
selectedIndex: _selectedIndex,
|
selectedIndex: _selectedIndex,
|
||||||
onDestinationSelected: (index) => setState(() => _selectedIndex = index),
|
onDestinationSelected: (index) {
|
||||||
|
setState(() => _selectedIndex = index);
|
||||||
|
if (index == 0) {
|
||||||
|
_loadSelectedTeam();
|
||||||
|
}
|
||||||
|
},
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
|
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
@@ -167,13 +216,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: _teamController.teamsStream,
|
stream: _teamController.teamsStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
// Correção: Verifica hasData para evitar piscar tela de loading
|
if (!snapshot.hasData && snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
|
||||||
if (!snapshot.hasData && snapshot.connectionState == ConnectionState.waiting) {
|
if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface))));
|
||||||
return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
|
|
||||||
}
|
|
||||||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
|
||||||
return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface))));
|
|
||||||
}
|
|
||||||
|
|
||||||
final teams = snapshot.data!;
|
final teams = snapshot.data!;
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
@@ -181,18 +225,33 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
itemCount: teams.length,
|
itemCount: teams.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final team = teams[index];
|
final team = teams[index];
|
||||||
|
final String? logoUrl = team['image_url'];
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.shield, color: AppTheme.primaryRed),
|
leading: ClipOval(
|
||||||
|
child: Container(
|
||||||
|
width: 36 * context.sf, height: 36 * context.sf, color: AppTheme.primaryRed.withOpacity(0.1),
|
||||||
|
child: (logoUrl != null && logoUrl.isNotEmpty)
|
||||||
|
? CachedNetworkImage(imageUrl: logoUrl, fit: BoxFit.cover, placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf))
|
||||||
|
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
title: Text(team['name'] ?? 'Sem Nome', style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
title: Text(team['name'] ?? 'Sem Nome', style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)),
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedTeamId = team['id'].toString();
|
_selectedTeamId = team['id'].toString();
|
||||||
_selectedTeamName = team['name'] ?? 'Desconhecido';
|
_selectedTeamName = team['name'] ?? 'Desconhecido';
|
||||||
|
_selectedTeamLogo = logoUrl;
|
||||||
_teamWins = int.tryParse(team['wins']?.toString() ?? '0') ?? 0;
|
_teamWins = int.tryParse(team['wins']?.toString() ?? '0') ?? 0;
|
||||||
_teamLosses = int.tryParse(team['losses']?.toString() ?? '0') ?? 0;
|
_teamLosses = int.tryParse(team['losses']?.toString() ?? '0') ?? 0;
|
||||||
_teamDraws = int.tryParse(team['draws']?.toString() ?? '0') ?? 0;
|
_teamDraws = int.tryParse(team['draws']?.toString() ?? '0') ?? 0;
|
||||||
|
|
||||||
|
// Dizemos à StatusPage que a equipa mudou alterando a chave!
|
||||||
|
_statusKey = DateTime.now().toString();
|
||||||
});
|
});
|
||||||
Navigator.pop(context);
|
|
||||||
|
await _saveSelectedTeam();
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -225,16 +284,14 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
onTap: () => _showTeamSelector(context),
|
onTap: () => _showTeamSelector(context),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.all(12 * context.sf),
|
padding: EdgeInsets.all(12 * context.sf),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2))),
|
||||||
color: Theme.of(context).cardTheme.color,
|
|
||||||
borderRadius: BorderRadius.circular(15 * context.sf),
|
|
||||||
border: Border.all(color: Colors.grey.withOpacity(0.2))
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
(_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty)
|
||||||
|
? ClipOval(child: CachedNetworkImage(imageUrl: _selectedTeamLogo!, width: 24 * context.sf, height: 24 * context.sf, fit: BoxFit.cover, placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf), errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf)))
|
||||||
|
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
||||||
SizedBox(width: 10 * context.sf),
|
SizedBox(width: 10 * context.sf),
|
||||||
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
|
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
|
||||||
]),
|
]),
|
||||||
@@ -263,17 +320,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statRebBg)),
|
Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statRebBg)),
|
||||||
SizedBox(width: 12 * context.sf),
|
SizedBox(width: 12 * context.sf),
|
||||||
Expanded(
|
Expanded(child: PieChartCard(victories: _teamWins, defeats: _teamLosses, draws: _teamDraws, title: 'DESEMPENHO', subtitle: 'Temporada', backgroundColor: AppTheme.statPieBg, sf: context.sf)),
|
||||||
child: PieChartCard(
|
|
||||||
victories: _teamWins,
|
|
||||||
defeats: _teamLosses,
|
|
||||||
draws: _teamDraws,
|
|
||||||
title: 'DESEMPENHO',
|
|
||||||
subtitle: 'Temporada',
|
|
||||||
backgroundColor: AppTheme.statPieBg,
|
|
||||||
sf: context.sf
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -284,45 +331,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
|
|
||||||
_selectedTeamName == "Selecionar Equipa"
|
_selectedTeamName == "Selecionar Equipa"
|
||||||
? Container(
|
? Container(
|
||||||
width: double.infinity,
|
width: double.infinity, padding: EdgeInsets.all(24.0 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color ?? Colors.white, borderRadius: BorderRadius.circular(16 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4))]),
|
||||||
padding: EdgeInsets.all(24.0 * context.sf),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardTheme.color ?? Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16 * context.sf),
|
|
||||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
|
||||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4))],
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(padding: EdgeInsets.all(18 * context.sf), decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.08), shape: BoxShape.circle), child: Icon(Icons.shield_outlined, color: AppTheme.primaryRed, size: 42 * context.sf)),
|
||||||
padding: EdgeInsets.all(18 * context.sf),
|
|
||||||
decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.08), shape: BoxShape.circle),
|
|
||||||
child: Icon(Icons.shield_outlined, color: AppTheme.primaryRed, size: 42 * context.sf),
|
|
||||||
),
|
|
||||||
SizedBox(height: 20 * context.sf),
|
SizedBox(height: 20 * context.sf),
|
||||||
Text("Nenhuma Equipa Ativa", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: textColor)),
|
Text("Nenhuma Equipa Ativa", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: textColor)),
|
||||||
SizedBox(height: 8 * context.sf),
|
SizedBox(height: 8 * context.sf),
|
||||||
Text(
|
Text("Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.", textAlign: TextAlign.center, style: TextStyle(fontSize: 13 * context.sf, color: Colors.grey.shade600, height: 1.4)),
|
||||||
"Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 13 * context.sf, color: Colors.grey.shade600, height: 1.4),
|
|
||||||
),
|
|
||||||
SizedBox(height: 24 * context.sf),
|
SizedBox(height: 24 * context.sf),
|
||||||
SizedBox(
|
SizedBox(width: double.infinity, height: 48 * context.sf, child: ElevatedButton.icon(onPressed: () => _showTeamSelector(context), style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), icon: Icon(Icons.touch_app, size: 20 * context.sf), label: Text("Selecionar Agora", style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.bold)))),
|
||||||
width: double.infinity,
|
|
||||||
height: 48 * context.sf,
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () => _showTeamSelector(context),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppTheme.primaryRed,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf)),
|
|
||||||
),
|
|
||||||
icon: Icon(Icons.touch_app, size: 20 * context.sf),
|
|
||||||
label: Text("Selecionar Agora", style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.bold)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -330,11 +348,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
stream: _supabase.from('games').stream(primaryKey: ['id']).order('game_date', ascending: false),
|
stream: _supabase.from('games').stream(primaryKey: ['id']).order('game_date', ascending: false),
|
||||||
builder: (context, gameSnapshot) {
|
builder: (context, gameSnapshot) {
|
||||||
if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
|
if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
|
||||||
|
if (!gameSnapshot.hasData && gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
|
||||||
// Correção: Verifica hasData em vez de ConnectionState para manter a lista na tela enquanto atualiza em plano de fundo
|
|
||||||
if (!gameSnapshot.hasData && gameSnapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
final todosOsJogos = gameSnapshot.data ?? [];
|
final todosOsJogos = gameSnapshot.data ?? [];
|
||||||
final gamesList = todosOsJogos.where((game) {
|
final gamesList = todosOsJogos.where((game) {
|
||||||
@@ -344,44 +358,19 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado';
|
return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado';
|
||||||
}).take(3).toList();
|
}).take(3).toList();
|
||||||
|
|
||||||
if (gamesList.isEmpty) {
|
if (gamesList.isEmpty) return Container(width: double.infinity, padding: EdgeInsets.all(20 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(14)), alignment: Alignment.center, child: const Text("Ainda não há jogos terminados.", style: TextStyle(color: Colors.grey)));
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: EdgeInsets.all(20 * context.sf),
|
|
||||||
decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(14)),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: const Text("Ainda não há jogos terminados.", style: TextStyle(color: Colors.grey)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: gamesList.map((game) {
|
children: gamesList.map((game) {
|
||||||
String dbMyTeam = game['my_team']?.toString() ?? '';
|
String dbMyTeam = game['my_team']?.toString() ?? ''; String dbOppTeam = game['opponent_team']?.toString() ?? '';
|
||||||
String dbOppTeam = game['opponent_team']?.toString() ?? '';
|
int dbMyScore = int.tryParse(game['my_score']?.toString() ?? '0') ?? 0; int dbOppScore = int.tryParse(game['opponent_score']?.toString() ?? '0') ?? 0;
|
||||||
int dbMyScore = int.tryParse(game['my_score']?.toString() ?? '0') ?? 0;
|
|
||||||
int dbOppScore = int.tryParse(game['opponent_score']?.toString() ?? '0') ?? 0;
|
|
||||||
|
|
||||||
String opponent; int myScore; int oppScore;
|
String opponent; int myScore; int oppScore;
|
||||||
|
|
||||||
if (dbMyTeam == _selectedTeamName) {
|
if (dbMyTeam == _selectedTeamName) { opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore; } else { opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore; }
|
||||||
opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore;
|
String rawDate = game['game_date']?.toString() ?? '---'; String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
|
||||||
} else {
|
String result = myScore > oppScore ? 'V' : (myScore < oppScore ? 'D' : 'E');
|
||||||
opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore;
|
|
||||||
}
|
|
||||||
|
|
||||||
String rawDate = game['game_date']?.toString() ?? '---';
|
|
||||||
String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
|
|
||||||
|
|
||||||
String result = 'E';
|
|
||||||
if (myScore > oppScore) result = 'V';
|
|
||||||
if (myScore < oppScore) result = 'D';
|
|
||||||
|
|
||||||
return _buildGameHistoryCard(
|
return _buildGameHistoryCard(context: context, opponent: opponent, result: result, myScore: myScore, oppScore: oppScore, date: date, topPts: game['top_pts_name'] ?? '---', topAst: game['top_ast_name'] ?? '---', topRbs: game['top_rbs_name'] ?? '---', topDef: game['top_def_name'] ?? '---', mvp: game['mvp_name'] ?? '---');
|
||||||
context: context, opponent: opponent, result: result,
|
|
||||||
myScore: myScore, oppScore: oppScore, date: date,
|
|
||||||
topPts: game['top_pts_name'] ?? '---', topAst: game['top_ast_name'] ?? '---',
|
|
||||||
topRbs: game['top_rbs_name'] ?? '---', topDef: game['top_def_name'] ?? '---', mvp: game['mvp_name'] ?? '---',
|
|
||||||
);
|
|
||||||
}).toList(),
|
}).toList(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -404,39 +393,20 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
astMap[pid] = (astMap[pid] ?? 0) + (int.tryParse(row['ast']?.toString() ?? '0') ?? 0);
|
astMap[pid] = (astMap[pid] ?? 0) + (int.tryParse(row['ast']?.toString() ?? '0') ?? 0);
|
||||||
rbsMap[pid] = (rbsMap[pid] ?? 0) + (int.tryParse(row['rbs']?.toString() ?? '0') ?? 0);
|
rbsMap[pid] = (rbsMap[pid] ?? 0) + (int.tryParse(row['rbs']?.toString() ?? '0') ?? 0);
|
||||||
}
|
}
|
||||||
|
if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0};
|
||||||
if (ptsMap.isEmpty) {
|
String getBest(Map<String, int> map) { if (map.isEmpty) return '---'; return namesMap[map.entries.reduce((a, b) => a.value > b.value ? a : b).key] ?? '---'; }
|
||||||
return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0};
|
int getBestVal(Map<String, int> map) { if (map.isEmpty) return 0; return map.values.reduce((a, b) => a > b ? a : b); }
|
||||||
}
|
return {'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap), 'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap), 'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)};
|
||||||
|
|
||||||
String getBest(Map<String, int> map) {
|
|
||||||
if (map.isEmpty) return '---';
|
|
||||||
var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key;
|
|
||||||
return namesMap[bestId] ?? '---';
|
|
||||||
}
|
|
||||||
|
|
||||||
int getBestVal(Map<String, int> map) {
|
|
||||||
if (map.isEmpty) return 0;
|
|
||||||
return map.values.reduce((a, b) => a > b ? a : b);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap),
|
|
||||||
'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap),
|
|
||||||
'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) {
|
Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 4, margin: EdgeInsets.zero,
|
elevation: 4, margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: AppTheme.warningAmber, width: 2) : BorderSide.none),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: AppTheme.warningAmber, width: 2) : BorderSide.none),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])),
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final double ch = constraints.maxHeight;
|
final double ch = constraints.maxHeight; final double cw = constraints.maxWidth;
|
||||||
final double cw = constraints.maxWidth;
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.all(cw * 0.06),
|
padding: EdgeInsets.all(cw * 0.06),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -444,23 +414,13 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis),
|
Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
SizedBox(height: ch * 0.011),
|
SizedBox(height: ch * 0.011),
|
||||||
SizedBox(
|
SizedBox(width: double.infinity, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)))),
|
||||||
width: double.infinity,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.scaleDown, alignment: Alignment.centerLeft,
|
|
||||||
child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Center(child: FittedBox(fit: BoxFit.scaleDown, child: Text(statValue, style: TextStyle(fontSize: ch * 0.18, fontWeight: FontWeight.bold, color: Colors.white, height: 1.0)))),
|
Center(child: FittedBox(fit: BoxFit.scaleDown, child: Text(statValue, style: TextStyle(fontSize: ch * 0.18, fontWeight: FontWeight.bold, color: Colors.white, height: 1.0)))),
|
||||||
SizedBox(height: ch * 0.015),
|
SizedBox(height: ch * 0.015),
|
||||||
Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))),
|
Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Container(
|
Container(width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035), decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)), child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold)))),
|
||||||
width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035),
|
|
||||||
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)),
|
|
||||||
child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold)))
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -470,33 +430,20 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildGameHistoryCard({
|
Widget _buildGameHistoryCard({required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date, required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp}) {
|
||||||
required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date,
|
bool isWin = result == 'V'; bool isDraw = result == 'E';
|
||||||
required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp
|
|
||||||
}) {
|
|
||||||
bool isWin = result == 'V';
|
|
||||||
bool isDraw = result == 'E';
|
|
||||||
Color statusColor = isWin ? AppTheme.successGreen : (isDraw ? AppTheme.warningAmber : AppTheme.oppTeamRed);
|
Color statusColor = isWin ? AppTheme.successGreen : (isDraw ? AppTheme.warningAmber : AppTheme.oppTeamRed);
|
||||||
final bgColor = Theme.of(context).cardTheme.color;
|
final bgColor = Theme.of(context).cardTheme.color; final textColor = Theme.of(context).colorScheme.onSurface;
|
||||||
final textColor = Theme.of(context).colorScheme.onSurface;
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsets.only(bottom: 14 * context.sf),
|
margin: EdgeInsets.only(bottom: 14 * context.sf), decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))]),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: bgColor, borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))],
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(14 * context.sf),
|
padding: EdgeInsets.all(14 * context.sf),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(width: 36 * context.sf, height: 36 * context.sf, decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle), child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)))),
|
||||||
width: 36 * context.sf, height: 36 * context.sf,
|
|
||||||
decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle),
|
|
||||||
child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf))),
|
|
||||||
),
|
|
||||||
SizedBox(width: 14 * context.sf),
|
SizedBox(width: 14 * context.sf),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -508,14 +455,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||||
Padding(
|
Padding(padding: EdgeInsets.symmetric(horizontal: 8 * context.sf), child: Container(padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), decoration: BoxDecoration(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), borderRadius: BorderRadius.circular(8)), child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: textColor)))),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf),
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf),
|
|
||||||
decoration: BoxDecoration(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), borderRadius: BorderRadius.circular(8)),
|
|
||||||
child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: textColor)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)),
|
Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -527,30 +467,14 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
),
|
),
|
||||||
Divider(height: 1, color: Colors.grey.withOpacity(0.1), thickness: 1.5),
|
Divider(height: 1, color: Colors.grey.withOpacity(0.1), thickness: 1.5),
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf),
|
width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))),
|
||||||
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(children: [Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)), Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef))]),
|
||||||
children: [
|
|
||||||
Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)),
|
|
||||||
Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 8 * context.sf),
|
SizedBox(height: 8 * context.sf),
|
||||||
Row(
|
Row(children: [Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)), Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs))]),
|
||||||
children: [
|
|
||||||
Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)),
|
|
||||||
Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 8 * context.sf),
|
SizedBox(height: 8 * context.sf),
|
||||||
Row(
|
Row(children: [Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)), const Expanded(child: SizedBox())]),
|
||||||
children: [
|
|
||||||
Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)),
|
|
||||||
const Expanded(child: SizedBox()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -562,20 +486,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, String label, String value, {bool isMvp = false}) {
|
Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, String label, String value, {bool isMvp = false}) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 14 * context.sf, color: color),
|
Icon(icon, size: 14 * context.sf, color: color), SizedBox(width: 4 * context.sf),
|
||||||
SizedBox(width: 4 * context.sf),
|
|
||||||
Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)),
|
Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)),
|
||||||
Expanded(
|
Expanded(child: Text(value, style: TextStyle(fontSize: 11 * context.sf, color: isMvp ? AppTheme.warningAmber : Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||||
child: Text(
|
|
||||||
value,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11 * context.sf,
|
|
||||||
color: isMvp ? AppTheme.warningAmber : Theme.of(context).colorScheme.onSurface,
|
|
||||||
fontWeight: FontWeight.bold
|
|
||||||
),
|
|
||||||
maxLines: 1, overflow: TextOverflow.ellipsis
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:pdf/widgets.dart' as pw;
|
|||||||
import 'package:printing/printing.dart';
|
import 'package:printing/printing.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
// Modelo local para os tiros
|
|
||||||
class _ShotDot {
|
class _ShotDot {
|
||||||
final double relX;
|
final double relX;
|
||||||
final double relY;
|
final double relY;
|
||||||
@@ -13,29 +12,22 @@ class _ShotDot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PdfExportService {
|
class PdfExportService {
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
// ENTRY POINT
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
static Future<void> generateAndPrintBoxScore({
|
static Future<void> generateAndPrintBoxScore({
|
||||||
required String gameId,
|
required String gameId,
|
||||||
required String myTeam,
|
required String myTeam,
|
||||||
required String opponentTeam,
|
required String opponentTeam,
|
||||||
required String myScore,
|
required String myScore,
|
||||||
required String opponentScore,
|
required String opponentScore,
|
||||||
required String season,
|
required String season,
|
||||||
|
required String targetTeam,
|
||||||
}) async {
|
}) async {
|
||||||
final supabase = Supabase.instance.client;
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
// ── Jogo ────────────────────────────────────────────────────────────────
|
// ── Jogo ────────────────────────────────────────────────────────────────
|
||||||
final gameData =
|
final gameData = await supabase.from('games').select().eq('id', gameId).single();
|
||||||
await supabase.from('games').select().eq('id', gameId).single();
|
|
||||||
|
|
||||||
// ── Equipas ─────────────────────────────────────────────────────────────
|
// ── Equipas ─────────────────────────────────────────────────────────────
|
||||||
final teamsData = await supabase
|
final teamsData = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
|
||||||
.from('teams')
|
|
||||||
.select('id, name')
|
|
||||||
.inFilter('name', [myTeam, opponentTeam]);
|
|
||||||
|
|
||||||
String? myTeamId;
|
String? myTeamId;
|
||||||
for (var t in teamsData) {
|
for (var t in teamsData) {
|
||||||
@@ -44,32 +36,19 @@ class PdfExportService {
|
|||||||
|
|
||||||
// ── Jogadores (Apenas a minha equipa) ───────────────────────────────────
|
// ── Jogadores (Apenas a minha equipa) ───────────────────────────────────
|
||||||
List<dynamic> myPlayers = myTeamId != null
|
List<dynamic> myPlayers = myTeamId != null
|
||||||
? await supabase
|
? await supabase.from('members').select().eq('team_id', myTeamId).eq('type', 'Jogador')
|
||||||
.from('members')
|
|
||||||
.select()
|
|
||||||
.eq('team_id', myTeamId)
|
|
||||||
.eq('type', 'Jogador')
|
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// ── Estatísticas ─────────────────────────────────────────────────────────
|
// ── Estatísticas ─────────────────────────────────────────────────────────
|
||||||
final statsData =
|
final statsData = await supabase.from('player_stats').select().eq('game_id', gameId);
|
||||||
await supabase.from('player_stats').select().eq('game_id', gameId);
|
|
||||||
Map<String, Map<String, dynamic>> statsMap = {};
|
Map<String, Map<String, dynamic>> statsMap = {};
|
||||||
for (var s in statsData) {
|
for (var s in statsData) {
|
||||||
statsMap[s['member_id'].toString()] = s;
|
statsMap[s['member_id'].toString()] = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tiros (para o mapa de calor da minha equipa) ──────────────────────
|
// ── Tiros ──────────────────────
|
||||||
final shotsData = await supabase
|
final shotsData = await supabase.from('shot_locations').select().eq('game_id', gameId);
|
||||||
.from('shot_locations')
|
final Set<String> myPlayerIds = myPlayers.map((p) => p['id'].toString()).toSet();
|
||||||
.select()
|
|
||||||
.eq('game_id', gameId);
|
|
||||||
|
|
||||||
// IDs da minha equipa
|
|
||||||
final Set<String> myPlayerIds =
|
|
||||||
myPlayers.map((p) => p['id'].toString()).toSet();
|
|
||||||
|
|
||||||
// Separa os tiros: todos da minha equipa, depois por jogador
|
|
||||||
final List<_ShotDot> myTeamShots = [];
|
final List<_ShotDot> myTeamShots = [];
|
||||||
final Map<String, List<_ShotDot>> shotsByPlayer = {};
|
final Map<String, List<_ShotDot>> shotsByPlayer = {};
|
||||||
|
|
||||||
@@ -86,16 +65,14 @@ class PdfExportService {
|
|||||||
shotsByPlayer.putIfAbsent(memberId, () => []).add(dot);
|
shotsByPlayer.putIfAbsent(memberId, () => []).add(dot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tabela de estatísticas (Apenas a minha equipa) ────────────────────
|
// ── Tabela de estatísticas ────────────────────
|
||||||
List<List<String>> myTeamTable =
|
List<List<String>> myTeamTable = _buildTeamTableData(myPlayers, statsMap);
|
||||||
_buildTeamTableData(myPlayers, statsMap);
|
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
// CONSTRUÇÃO DO PDF
|
// CONSTRUÇÃO DO PDF
|
||||||
// ════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
final pdf = pw.Document();
|
final pdf = pw.Document();
|
||||||
|
|
||||||
// ── PÁGINA 1: Box Score ──────────────────────────────────────────────
|
|
||||||
pdf.addPage(
|
pdf.addPage(
|
||||||
pw.Page(
|
pw.Page(
|
||||||
pageFormat: PdfPageFormat.a4.landscape,
|
pageFormat: PdfPageFormat.a4.landscape,
|
||||||
@@ -110,71 +87,81 @@ class PdfExportService {
|
|||||||
children: [
|
children: [
|
||||||
pw.Row(
|
pw.Row(
|
||||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
pw.Text('Relatório Estatístico',
|
pw.Column(
|
||||||
style: pw.TextStyle(
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
fontSize: 22,
|
children: [
|
||||||
fontWeight: pw.FontWeight.bold)),
|
pw.Text('Relatório Estatístico', style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold)),
|
||||||
|
pw.SizedBox(height: 10),
|
||||||
|
pw.Text('Equipa: $myTeam', style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold, color: const PdfColor.fromInt(0xFFA00000))),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
pw.Column(
|
pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
pw.Text('$myTeam vs $opponentTeam',
|
pw.Text('$myTeam vs $opponentTeam', style: pw.TextStyle(fontSize: 15, fontWeight: pw.FontWeight.bold)),
|
||||||
style: pw.TextStyle(
|
pw.Text('Resultado: $myScore — $opponentScore', style: const pw.TextStyle(fontSize: 13)),
|
||||||
fontSize: 15,
|
pw.Text('Época: $season', style: const pw.TextStyle(fontSize: 11)),
|
||||||
fontWeight: pw.FontWeight.bold)),
|
pw.SizedBox(height: 10),
|
||||||
pw.Text('Resultado: $myScore — $opponentScore',
|
|
||||||
style: const pw.TextStyle(fontSize: 13)),
|
// 👇 NOVA TABELA: PONTUAÇÃO POR PERÍODO 👇
|
||||||
pw.Text('Época: $season',
|
pw.Table.fromTextArray(
|
||||||
style: const pw.TextStyle(fontSize: 11)),
|
context: context,
|
||||||
|
border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
|
||||||
|
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 8),
|
||||||
|
cellStyle: const pw.TextStyle(fontSize: 8),
|
||||||
|
headerDecoration: const pw.BoxDecoration(color: PdfColors.grey200),
|
||||||
|
cellAlignment: pw.Alignment.center,
|
||||||
|
data: <List<String>>[
|
||||||
|
['Equipa', '1ºQ', '2ºQ', '3ºQ', '4ºQ', 'F'],
|
||||||
|
[
|
||||||
|
myTeam,
|
||||||
|
gameData['my_q1']?.toString() ?? '-',
|
||||||
|
gameData['my_q2']?.toString() ?? '-',
|
||||||
|
gameData['my_q3']?.toString() ?? '-',
|
||||||
|
gameData['my_q4']?.toString() ?? '-',
|
||||||
|
myScore
|
||||||
|
],
|
||||||
|
[
|
||||||
|
opponentTeam,
|
||||||
|
gameData['opp_q1']?.toString() ?? '-',
|
||||||
|
gameData['opp_q2']?.toString() ?? '-',
|
||||||
|
gameData['opp_q3']?.toString() ?? '-',
|
||||||
|
gameData['opp_q4']?.toString() ?? '-',
|
||||||
|
opponentScore
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
pw.SizedBox(height: 12),
|
|
||||||
|
|
||||||
pw.Text('Equipa: $myTeam',
|
|
||||||
style: pw.TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: pw.FontWeight.bold,
|
|
||||||
color: const PdfColor.fromInt(0xFFA00000))),
|
|
||||||
pw.SizedBox(height: 8),
|
pw.SizedBox(height: 8),
|
||||||
|
|
||||||
pw.Text('Pontos e Lançamentos',
|
pw.Text('Pontos e Lançamentos', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
|
||||||
style: pw.TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: pw.FontWeight.bold,
|
|
||||||
color: PdfColors.grey700)),
|
|
||||||
pw.SizedBox(height: 2),
|
pw.SizedBox(height: 2),
|
||||||
_buildPdfTablePart1(
|
_buildPdfTablePart1(myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
||||||
myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
|
||||||
|
|
||||||
pw.SizedBox(height: 14),
|
pw.SizedBox(height: 14),
|
||||||
|
|
||||||
pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)',
|
pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
|
||||||
style: pw.TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: pw.FontWeight.bold,
|
|
||||||
color: PdfColors.grey700)),
|
|
||||||
pw.SizedBox(height: 2),
|
pw.SizedBox(height: 2),
|
||||||
_buildPdfTablePart2(
|
_buildPdfTablePart2(myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
||||||
myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
|
||||||
|
|
||||||
pw.SizedBox(height: 16),
|
pw.SizedBox(height: 16),
|
||||||
|
|
||||||
pw.Row(
|
pw.Row(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildSummaryBox('Melhor Marcador',
|
_buildSummaryBox('Melhor Marcador', gameData['top_pts_name'] ?? '---'),
|
||||||
gameData['top_pts_name'] ?? '---'),
|
|
||||||
pw.SizedBox(width: 10),
|
pw.SizedBox(width: 10),
|
||||||
_buildSummaryBox('Melhor Ressaltador',
|
_buildSummaryBox('Melhor Ressaltador', gameData['top_rbs_name'] ?? '---'),
|
||||||
gameData['top_rbs_name'] ?? '---'),
|
|
||||||
pw.SizedBox(width: 10),
|
pw.SizedBox(width: 10),
|
||||||
_buildSummaryBox('Melhor Passador',
|
_buildSummaryBox('Melhor Passador', gameData['top_ast_name'] ?? '---'),
|
||||||
gameData['top_ast_name'] ?? '---'),
|
|
||||||
pw.SizedBox(width: 10),
|
pw.SizedBox(width: 10),
|
||||||
_buildSummaryBox(
|
_buildSummaryBox('MVP', gameData['mvp_name'] ?? '---'),
|
||||||
'MVP', gameData['mvp_name'] ?? '---'),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -195,15 +182,13 @@ class PdfExportService {
|
|||||||
return pw.Column(
|
return pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)',
|
_heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)', const PdfColor.fromInt(0xFFA00000)),
|
||||||
const PdfColor.fromInt(0xFFA00000)),
|
|
||||||
pw.SizedBox(height: 12),
|
pw.SizedBox(height: 12),
|
||||||
pw.Expanded(
|
pw.Expanded(
|
||||||
child: pw.Center(
|
child: pw.Center(
|
||||||
child: pw.CustomPaint(
|
child: pw.CustomPaint(
|
||||||
size: const PdfPoint(360, 360),
|
size: const PdfPoint(360, 360),
|
||||||
painter: (canvas, size) =>
|
painter: (canvas, size) => _paintCourt(canvas, size, myTeamShots),
|
||||||
_paintCourt(canvas, size, myTeamShots),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -217,7 +202,6 @@ class PdfExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── PÁGINAS 3+: Mapa de Calor por Jogador (4 por folha) ──────────────
|
// ── PÁGINAS 3+: Mapa de Calor por Jogador (4 por folha) ──────────────
|
||||||
// 👇 FILTRO ATIVO: Só entra aqui quem tiver tiros na lista "shotsByPlayer"!
|
|
||||||
final activePlayers = myPlayers.where((p) {
|
final activePlayers = myPlayers.where((p) {
|
||||||
final pid = p['id'].toString();
|
final pid = p['id'].toString();
|
||||||
return shotsByPlayer[pid] != null && shotsByPlayer[pid]!.isNotEmpty;
|
return shotsByPlayer[pid] != null && shotsByPlayer[pid]!.isNotEmpty;
|
||||||
@@ -280,9 +264,6 @@ class PdfExportService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
// WIDGET DO MAPA DE CALOR INDIVIDUAL
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
static pw.Widget _buildPlayerHeatmap(dynamic player, List<_ShotDot> shots, Map<String, dynamic> stats) {
|
static pw.Widget _buildPlayerHeatmap(dynamic player, List<_ShotDot> shots, Map<String, dynamic> stats) {
|
||||||
final String playerName = player['name']?.toString() ?? 'Jogador';
|
final String playerName = player['name']?.toString() ?? 'Jogador';
|
||||||
final String playerNumber = player['number']?.toString() ?? '0';
|
final String playerNumber = player['number']?.toString() ?? '0';
|
||||||
@@ -320,16 +301,11 @@ class PdfExportService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
// CORREÇÃO: DESENHO DO CAMPO E LINHAS ADAPTADAS
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
static void _paintCourt(PdfGraphics canvas, PdfPoint size, List<_ShotDot> shots) {
|
static void _paintCourt(PdfGraphics canvas, PdfPoint size, List<_ShotDot> shots) {
|
||||||
final double w = size.x;
|
final double w = size.x;
|
||||||
final double h = size.y;
|
final double h = size.y;
|
||||||
final double basketX = w / 2;
|
final double basketX = w / 2;
|
||||||
|
|
||||||
// Fundo Amarelo (Toda a área)
|
|
||||||
canvas
|
canvas
|
||||||
..setFillColor(const PdfColor.fromInt(0xFFDFAB00))
|
..setFillColor(const PdfColor.fromInt(0xFFDFAB00))
|
||||||
..drawRect(0, 0, w, h)
|
..drawRect(0, 0, w, h)
|
||||||
@@ -341,7 +317,6 @@ class PdfExportService {
|
|||||||
final double alturaDoArco = larguraDoArco * 0.30;
|
final double alturaDoArco = larguraDoArco * 0.30;
|
||||||
final double totalArcoHeight = alturaDoArco * 4;
|
final double totalArcoHeight = alturaDoArco * 4;
|
||||||
|
|
||||||
// ── 1. LINHAS BRANCAS ───────────────────────────────────────────────
|
|
||||||
canvas.setStrokeColor(PdfColors.white);
|
canvas.setStrokeColor(PdfColors.white);
|
||||||
canvas.setLineWidth(2.0);
|
canvas.setLineWidth(2.0);
|
||||||
|
|
||||||
@@ -350,7 +325,6 @@ class PdfExportService {
|
|||||||
_drawLine(canvas, h, 0, length, margin, length);
|
_drawLine(canvas, h, 0, length, margin, length);
|
||||||
_drawLine(canvas, h, w - margin, length, w, length);
|
_drawLine(canvas, h, w - margin, length, w, length);
|
||||||
|
|
||||||
// Arco 3pts
|
|
||||||
_drawEllipseArc(canvas, h, basketX, length, larguraDoArco, totalArcoHeight / 2, 0, math.pi);
|
_drawEllipseArc(canvas, h, basketX, length, larguraDoArco, totalArcoHeight / 2, 0, math.pi);
|
||||||
|
|
||||||
double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75));
|
double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75));
|
||||||
@@ -361,46 +335,34 @@ class PdfExportService {
|
|||||||
_drawLine(canvas, h, sXL, sYL, 0, h * 0.85);
|
_drawLine(canvas, h, sXL, sYL, 0, h * 0.85);
|
||||||
_drawLine(canvas, h, sXR, sYR, w, h * 0.85);
|
_drawLine(canvas, h, sXR, sYR, w, h * 0.85);
|
||||||
|
|
||||||
// ── 2. LINHAS PRETAS ─────────────────────────────────────────────────
|
|
||||||
canvas.setStrokeColor(PdfColors.black);
|
canvas.setStrokeColor(PdfColors.black);
|
||||||
canvas.setLineWidth(1.5);
|
canvas.setLineWidth(1.5);
|
||||||
|
|
||||||
final double pW = w * 0.28;
|
final double pW = w * 0.28;
|
||||||
final double pH = h * 0.38;
|
final double pH = h * 0.38;
|
||||||
|
|
||||||
// Garrafão
|
|
||||||
_drawRect(canvas, h, basketX - pW / 2, 0, pW, pH);
|
_drawRect(canvas, h, basketX - pW / 2, 0, pW, pH);
|
||||||
|
|
||||||
// Círculo Lances Livres
|
|
||||||
final double ftR = pW / 2;
|
final double ftR = pW / 2;
|
||||||
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, 0, math.pi);
|
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, 0, math.pi);
|
||||||
// Tracejado
|
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, math.pi + (i * 2 * (math.pi / 20)), math.pi / 20);
|
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, math.pi + (i * 2 * (math.pi / 20)), math.pi / 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linhas oblíquas do garrafão
|
|
||||||
_drawLine(canvas, h, basketX - pW / 2, pH, sXL, sYL);
|
_drawLine(canvas, h, basketX - pW / 2, pH, sXL, sYL);
|
||||||
_drawLine(canvas, h, basketX + pW / 2, pH, sXR, sYR);
|
_drawLine(canvas, h, basketX + pW / 2, pH, sXR, sYR);
|
||||||
|
|
||||||
// Meio Campo
|
|
||||||
_drawEllipseArc(canvas, h, basketX, h, w * 0.12, w * 0.12, math.pi, math.pi);
|
_drawEllipseArc(canvas, h, basketX, h, w * 0.12, w * 0.12, math.pi, math.pi);
|
||||||
|
|
||||||
// Cesto e Tabela
|
|
||||||
_drawCircle(canvas, h, basketX, h * 0.12, w * 0.02);
|
_drawCircle(canvas, h, basketX, h * 0.12, w * 0.02);
|
||||||
_drawLine(canvas, h, basketX - w * 0.08, h * 0.12 - 5, basketX + w * 0.08, h * 0.12 - 5);
|
_drawLine(canvas, h, basketX - w * 0.08, h * 0.12 - 5, basketX + w * 0.08, h * 0.12 - 5);
|
||||||
|
|
||||||
// ── 3. TIROS ─────────────────────────────────────────────────────────
|
|
||||||
for (final shot in shots) {
|
for (final shot in shots) {
|
||||||
final double px = shot.relX * w;
|
final double px = shot.relX * w;
|
||||||
final double py = shot.relY * h;
|
final double py = shot.relY * h;
|
||||||
|
|
||||||
final PdfColor dotColor = shot.isMake ? PdfColors.green600 : PdfColors.red600;
|
final PdfColor dotColor = shot.isMake ? PdfColors.green600 : PdfColors.red600;
|
||||||
|
|
||||||
// Desenha Círculo Colorido
|
|
||||||
_fillCircle(canvas, h, px, py, 6, dotColor);
|
_fillCircle(canvas, h, px, py, 6, dotColor);
|
||||||
|
|
||||||
// Símbolos
|
|
||||||
canvas.setStrokeColor(PdfColors.white);
|
canvas.setStrokeColor(PdfColors.white);
|
||||||
canvas.setLineWidth(1.2);
|
canvas.setLineWidth(1.2);
|
||||||
if (shot.isMake) {
|
if (shot.isMake) {
|
||||||
@@ -413,19 +375,12 @@ class PdfExportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers com inversão automática do Eixo Y para casar com Flutter ──
|
|
||||||
static void _drawLine(PdfGraphics c, double canvasH, double x1, double y1, double x2, double y2) {
|
static void _drawLine(PdfGraphics c, double canvasH, double x1, double y1, double x2, double y2) {
|
||||||
c.moveTo(x1, canvasH - y1);
|
c.moveTo(x1, canvasH - y1);
|
||||||
c.lineTo(x2, canvasH - y2);
|
c.lineTo(x2, canvasH - y2);
|
||||||
c.strokePath();
|
c.strokePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _lineRaw(PdfGraphics c, double x1, double y1, double x2, double y2) {
|
|
||||||
c.moveTo(x1, y1);
|
|
||||||
c.lineTo(x2, y2);
|
|
||||||
c.strokePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _drawRect(PdfGraphics c, double canvasH, double x, double y, double width, double height) {
|
static void _drawRect(PdfGraphics c, double canvasH, double x, double y, double width, double height) {
|
||||||
c.drawRect(x, canvasH - (y + height), width, height);
|
c.drawRect(x, canvasH - (y + height), width, height);
|
||||||
c.strokePath();
|
c.strokePath();
|
||||||
@@ -460,12 +415,7 @@ class PdfExportService {
|
|||||||
c.strokePath();
|
c.strokePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
static List<List<String>> _buildTeamTableData(List<dynamic> players, Map<String, Map<String, dynamic>> statsMap) {
|
||||||
// TABELAS DE ESTATÍSTICAS
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
static List<List<String>> _buildTeamTableData(
|
|
||||||
List<dynamic> players, Map<String, Map<String, dynamic>> statsMap) {
|
|
||||||
List<List<String>> tableData = [];
|
List<List<String>> tableData = [];
|
||||||
|
|
||||||
int tPts = 0, tFgm = 0, tFga = 0, tFtm = 0, tFta = 0, tFls = 0;
|
int tPts = 0, tFgm = 0, tFga = 0, tFtm = 0, tFta = 0, tFls = 0;
|
||||||
@@ -485,27 +435,16 @@ class PdfExportService {
|
|||||||
var s = statsMap[id] ?? {};
|
var s = statsMap[id] ?? {};
|
||||||
|
|
||||||
int pts = s['pts'] ?? 0;
|
int pts = s['pts'] ?? 0;
|
||||||
int fgm = s['fgm'] ?? 0;
|
int fgm = s['fgm'] ?? 0; int fga = s['fga'] ?? 0;
|
||||||
int fga = s['fga'] ?? 0;
|
int ftm = s['ftm'] ?? 0; int fta = s['fta'] ?? 0;
|
||||||
int ftm = s['ftm'] ?? 0;
|
int p2m = s['p2m'] ?? 0; int p2a = s['p2a'] ?? 0;
|
||||||
int fta = s['fta'] ?? 0;
|
int p3m = s['p3m'] ?? 0; int p3a = s['p3a'] ?? 0;
|
||||||
int p2m = s['p2m'] ?? 0;
|
|
||||||
int p2a = s['p2a'] ?? 0;
|
|
||||||
int p3m = s['p3m'] ?? 0;
|
|
||||||
int p3a = s['p3a'] ?? 0;
|
|
||||||
int fls = s['fls'] ?? 0;
|
int fls = s['fls'] ?? 0;
|
||||||
int orb = s['orb'] ?? 0;
|
int orb = s['orb'] ?? 0; int drb = s['drb'] ?? 0;
|
||||||
int drb = s['drb'] ?? 0;
|
int stl = s['stl'] ?? 0; int ast = s['ast'] ?? 0;
|
||||||
int stl = s['stl'] ?? 0;
|
int tov = s['tov'] ?? 0; int blk = s['blk'] ?? 0;
|
||||||
int ast = s['ast'] ?? 0;
|
int so = s['so'] ?? 0; int il = s['il'] ?? 0; int li = s['li'] ?? 0;
|
||||||
int tov = s['tov'] ?? 0;
|
int pa = s['pa'] ?? 0; int tresS = s['tres_seg'] ?? 0; int dr = s['dr'] ?? 0;
|
||||||
int blk = s['blk'] ?? 0;
|
|
||||||
int so = s['so'] ?? 0;
|
|
||||||
int il = s['il'] ?? 0;
|
|
||||||
int li = s['li'] ?? 0;
|
|
||||||
int pa = s['pa'] ?? 0;
|
|
||||||
int tresS = s['tres_seg'] ?? 0;
|
|
||||||
int dr = s['dr'] ?? 0;
|
|
||||||
int sec = s['minutos_jogados'] ?? 0;
|
int sec = s['minutos_jogados'] ?? 0;
|
||||||
|
|
||||||
tPts += pts; tFgm += fgm; tFga += fga; tFtm += ftm; tFta += fta;
|
tPts += pts; tFgm += fgm; tFga += fga; tFtm += ftm; tFta += fta;
|
||||||
@@ -525,8 +464,7 @@ class PdfExportService {
|
|||||||
tableData.add([
|
tableData.add([
|
||||||
p['number']?.toString() ?? '-',
|
p['number']?.toString() ?? '-',
|
||||||
p['name']?.toString() ?? '?',
|
p['name']?.toString() ?? '?',
|
||||||
minStr,
|
minStr, pts.toString(),
|
||||||
pts.toString(),
|
|
||||||
p2m.toString(), p2a.toString(), p2Pct,
|
p2m.toString(), p2a.toString(), p2Pct,
|
||||||
p3m.toString(), p3a.toString(), p3Pct,
|
p3m.toString(), p3a.toString(), p3Pct,
|
||||||
fgm.toString(), fga.toString(), fgPct,
|
fgm.toString(), fga.toString(), fgPct,
|
||||||
@@ -717,8 +655,7 @@ class PdfExportService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static pw.Widget _groupHeader(
|
static pw.Widget _groupHeader(String title, pw.TextStyle hStyle, pw.TextStyle sStyle) {
|
||||||
String title, pw.TextStyle hStyle, pw.TextStyle sStyle) {
|
|
||||||
return pw.Column(
|
return pw.Column(
|
||||||
children: [
|
children: [
|
||||||
pw.Container(
|
pw.Container(
|
||||||
@@ -726,54 +663,28 @@ class PdfExportService {
|
|||||||
alignment: pw.Alignment.center,
|
alignment: pw.Alignment.center,
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
||||||
decoration: const pw.BoxDecoration(
|
decoration: const pw.BoxDecoration(
|
||||||
border: pw.Border(
|
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)),
|
||||||
bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)),
|
|
||||||
),
|
),
|
||||||
child: pw.Text(title, style: hStyle),
|
child: pw.Text(title, style: hStyle),
|
||||||
),
|
),
|
||||||
pw.Row(children: [
|
pw.Row(children: [
|
||||||
pw.Expanded(
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('C', style: sStyle))),
|
||||||
child: pw.Container(
|
|
||||||
alignment: pw.Alignment.center,
|
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: pw.Text('C', style: sStyle))),
|
|
||||||
pw.Container(width: 0.5, height: 10, color: PdfColors.white),
|
pw.Container(width: 0.5, height: 10, color: PdfColors.white),
|
||||||
pw.Expanded(
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('T', style: sStyle))),
|
||||||
child: pw.Container(
|
|
||||||
alignment: pw.Alignment.center,
|
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: pw.Text('T', style: sStyle))),
|
|
||||||
pw.Container(width: 0.5, height: 10, color: PdfColors.white),
|
pw.Container(width: 0.5, height: 10, color: PdfColors.white),
|
||||||
pw.Expanded(
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('%', style: sStyle))),
|
||||||
child: pw.Container(
|
|
||||||
alignment: pw.Alignment.center,
|
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: pw.Text('%', style: sStyle))),
|
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static pw.Widget _groupData(
|
static pw.Widget _groupData(String c, String t, String pct, pw.TextStyle style) {
|
||||||
String c, String t, String pct, pw.TextStyle style) {
|
|
||||||
return pw.Row(children: [
|
return pw.Row(children: [
|
||||||
pw.Expanded(
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(c, style: style))),
|
||||||
child: pw.Container(
|
|
||||||
alignment: pw.Alignment.center,
|
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: pw.Text(c, style: style))),
|
|
||||||
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
|
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
|
||||||
pw.Expanded(
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(t, style: style))),
|
||||||
child: pw.Container(
|
|
||||||
alignment: pw.Alignment.center,
|
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: pw.Text(t, style: style))),
|
|
||||||
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
|
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
|
||||||
pw.Expanded(
|
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(pct, style: style))),
|
||||||
child: pw.Container(
|
|
||||||
alignment: pw.Alignment.center,
|
|
||||||
padding: const pw.EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: pw.Text(pct, style: style))),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,17 +692,8 @@ class PdfExportService {
|
|||||||
return pw.Container(
|
return pw.Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
decoration: pw.BoxDecoration(
|
decoration: pw.BoxDecoration(color: color, borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6))),
|
||||||
color: color,
|
child: pw.Text(title, style: pw.TextStyle(color: PdfColors.white, fontSize: 14, fontWeight: pw.FontWeight.bold)),
|
||||||
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6)),
|
|
||||||
),
|
|
||||||
child: pw.Text(
|
|
||||||
title,
|
|
||||||
style: pw.TextStyle(
|
|
||||||
color: PdfColors.white,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: pw.FontWeight.bold),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,15 +701,11 @@ class PdfExportService {
|
|||||||
return pw.Row(
|
return pw.Row(
|
||||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
pw.Container(width: 12, height: 12,
|
pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.green600, shape: pw.BoxShape.circle)),
|
||||||
decoration: const pw.BoxDecoration(
|
|
||||||
color: PdfColors.green600, shape: pw.BoxShape.circle)),
|
|
||||||
pw.SizedBox(width: 4),
|
pw.SizedBox(width: 4),
|
||||||
pw.Text('Cesto marcado', style: pw.TextStyle(fontSize: 10)),
|
pw.Text('Cesto marcado', style: pw.TextStyle(fontSize: 10)),
|
||||||
pw.SizedBox(width: 20),
|
pw.SizedBox(width: 20),
|
||||||
pw.Container(width: 12, height: 12,
|
pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.red600, shape: pw.BoxShape.circle)),
|
||||||
decoration: const pw.BoxDecoration(
|
|
||||||
color: PdfColors.red600, shape: pw.BoxShape.circle)),
|
|
||||||
pw.SizedBox(width: 4),
|
pw.SizedBox(width: 4),
|
||||||
pw.Text('Cesto falhado', style: pw.TextStyle(fontSize: 10)),
|
pw.Text('Cesto falhado', style: pw.TextStyle(fontSize: 10)),
|
||||||
],
|
],
|
||||||
@@ -817,28 +715,15 @@ class PdfExportService {
|
|||||||
static pw.Widget _buildSummaryBox(String title, String value) {
|
static pw.Widget _buildSummaryBox(String title, String value) {
|
||||||
return pw.Container(
|
return pw.Container(
|
||||||
width: 120,
|
width: 120,
|
||||||
decoration: pw.BoxDecoration(
|
decoration: pw.BoxDecoration(border: pw.TableBorder.all(color: PdfColors.black, width: 1)),
|
||||||
border: pw.TableBorder.all(color: PdfColors.black, width: 1),
|
|
||||||
),
|
|
||||||
child: pw.Column(children: [
|
child: pw.Column(children: [
|
||||||
pw.Container(
|
pw.Container(
|
||||||
width: double.infinity,
|
width: double.infinity, padding: const pw.EdgeInsets.all(6), color: const PdfColor.fromInt(0xFFA00000),
|
||||||
padding: const pw.EdgeInsets.all(6),
|
child: pw.Text(title, style: pw.TextStyle(color: PdfColors.white, fontSize: 9, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center),
|
||||||
color: const PdfColor.fromInt(0xFFA00000),
|
|
||||||
child: pw.Text(title,
|
|
||||||
style: pw.TextStyle(
|
|
||||||
color: PdfColors.white,
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: pw.FontWeight.bold),
|
|
||||||
textAlign: pw.TextAlign.center),
|
|
||||||
),
|
),
|
||||||
pw.Container(
|
pw.Container(
|
||||||
width: double.infinity,
|
width: double.infinity, padding: const pw.EdgeInsets.all(8),
|
||||||
padding: const pw.EdgeInsets.all(8),
|
child: pw.Text(value, style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center),
|
||||||
child: pw.Text(value,
|
|
||||||
style: pw.TextStyle(
|
|
||||||
fontSize: 10, fontWeight: pw.FontWeight.bold),
|
|
||||||
textAlign: pw.TextAlign.center),
|
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart'; // 👇 IMPORTAÇÃO PARA CACHE
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart'; // 👇 IMPORTAÇÃO PARA MEMÓRIA RÁPIDA
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../utils/size_extension.dart';
|
import '../utils/size_extension.dart';
|
||||||
import 'login.dart';
|
import 'login.dart';
|
||||||
@@ -23,7 +23,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
File? _localImageFile;
|
File? _localImageFile;
|
||||||
String? _uploadedImageUrl;
|
String? _uploadedImageUrl;
|
||||||
bool _isUploadingImage = false;
|
bool _isUploadingImage = false;
|
||||||
bool _isMemoryLoaded = false; // 👇 VARIÁVEL MÁGICA CONTRA O PISCAR
|
bool _isMemoryLoaded = false;
|
||||||
|
|
||||||
final supabase = Supabase.instance.client;
|
final supabase = Supabase.instance.client;
|
||||||
|
|
||||||
@@ -33,16 +33,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_loadUserAvatar();
|
_loadUserAvatar();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👇 LÊ A IMAGEM DA MEMÓRIA INSTANTANEAMENTE E CONFIRMA NA BD
|
|
||||||
Future<void> _loadUserAvatar() async {
|
Future<void> _loadUserAvatar() async {
|
||||||
// 1. Lê da memória rápida primeiro!
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final savedUrl = prefs.getString('meu_avatar_guardado');
|
final savedUrl = prefs.getString('meu_avatar_guardado');
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (savedUrl != null) _uploadedImageUrl = savedUrl;
|
if (savedUrl != null) _uploadedImageUrl = savedUrl;
|
||||||
_isMemoryLoaded = true; // Avisa que já leu a memória
|
_isMemoryLoaded = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +57,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (mounted && data != null && data['avatar_url'] != null) {
|
if (mounted && data != null && data['avatar_url'] != null) {
|
||||||
final urlDoSupabase = data['avatar_url'];
|
final urlDoSupabase = data['avatar_url'];
|
||||||
|
|
||||||
// Atualiza a memória se a foto na base de dados for diferente
|
|
||||||
if (urlDoSupabase != savedUrl) {
|
if (urlDoSupabase != savedUrl) {
|
||||||
await prefs.setString('meu_avatar_guardado', urlDoSupabase);
|
await prefs.setString('meu_avatar_guardado', urlDoSupabase);
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -68,7 +65,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Erro ao carregar avatar: $e");
|
debugPrint("Erro ao carregar avatar: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +92,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
fileOptions: const FileOptions(cacheControl: '3600', upsert: true)
|
fileOptions: const FileOptions(cacheControl: '3600', upsert: true)
|
||||||
);
|
);
|
||||||
|
|
||||||
final String publicUrl = supabase.storage.from('avatars').getPublicUrl(storagePath);
|
// 👇 TRUQUE MÁGICO PARA O AVATAR ATUALIZAR: Adicionar o timestamp ao URL!
|
||||||
|
final String baseUrl = supabase.storage.from('avatars').getPublicUrl(storagePath);
|
||||||
|
final String publicUrl = '$baseUrl?v=${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
await supabase
|
await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
@@ -104,7 +103,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
'avatar_url': publicUrl
|
'avatar_url': publicUrl
|
||||||
});
|
});
|
||||||
|
|
||||||
// 👇 MÁGICA: GUARDA LOGO O NOVO URL NA MEMÓRIA PARA A HOME SABER!
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString('meu_avatar_guardado', publicUrl);
|
await prefs.setString('meu_avatar_guardado', publicUrl);
|
||||||
|
|
||||||
@@ -280,7 +278,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👇 AVATAR OTIMIZADO: SEM LAG, COM CACHE E MEMÓRIA
|
|
||||||
Widget _buildTappableProfileAvatar(BuildContext context, Color primaryRed) {
|
Widget _buildTappableProfileAvatar(BuildContext context, Color primaryRed) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -298,29 +295,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
child: ClipOval(
|
child: ClipOval(
|
||||||
child: _isUploadingImage && _localImageFile != null
|
child: _isUploadingImage && _localImageFile != null
|
||||||
// 1. Mostrar imagem local (galeria) ENQUANTO está a fazer upload
|
|
||||||
? Image.file(_localImageFile!, fit: BoxFit.cover)
|
? Image.file(_localImageFile!, fit: BoxFit.cover)
|
||||||
|
|
||||||
// 2. Antes da memória carregar, fica só o fundo (evita piscar)
|
|
||||||
: !_isMemoryLoaded
|
: !_isMemoryLoaded
|
||||||
? const SizedBox()
|
? const SizedBox()
|
||||||
|
|
||||||
// 3. Depois da memória carregar, se houver URL, desenha com Cache!
|
|
||||||
: _uploadedImageUrl != null && _uploadedImageUrl!.isNotEmpty
|
: _uploadedImageUrl != null && _uploadedImageUrl!.isNotEmpty
|
||||||
? CachedNetworkImage(
|
? CachedNetworkImage(
|
||||||
imageUrl: _uploadedImageUrl!,
|
imageUrl: _uploadedImageUrl!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
fadeInDuration: Duration.zero, // Fica instantâneo!
|
fadeInDuration: Duration.zero,
|
||||||
placeholder: (context, url) => const SizedBox(),
|
placeholder: (context, url) => const SizedBox(),
|
||||||
errorWidget: (context, url, error) => Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
|
errorWidget: (context, url, error) => Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 4. Se não houver URL, mete o boneco
|
|
||||||
: Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
|
: Icon(Icons.person, color: primaryRed, size: 36 * context.sf),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ÍCONE DE LÁPIS
|
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
@@ -335,7 +324,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// LOADING OVERLAY (Enquanto faz o upload)
|
|
||||||
if (_isUploadingImage)
|
if (_isUploadingImage)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -364,9 +352,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// Limpa a memória do Avatar ao sair para não aparecer na conta de outra pessoa!
|
// 👇 AGORA LIMPA A EQUIPA E TUDO DA MEMÓRIA AO SAIR!
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.remove('meu_avatar_guardado');
|
await prefs.remove('meu_avatar_guardado');
|
||||||
|
await prefs.remove('last_team_id');
|
||||||
|
await prefs.remove('last_team_name');
|
||||||
|
await prefs.remove('last_team_logo');
|
||||||
|
await prefs.remove('last_team_wins');
|
||||||
|
await prefs.remove('last_team_losses');
|
||||||
|
await prefs.remove('last_team_draws');
|
||||||
|
|
||||||
await Supabase.instance.client.auth.signOut();
|
await Supabase.instance.client.auth.signOut();
|
||||||
if (ctx.mounted) {
|
if (ctx.mounted) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:playmaker/classe/theme.dart';
|
import 'package:playmaker/classe/theme.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart'; // 👇 A MAGIA DO CACHE
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../utils/size_extension.dart';
|
import '../utils/size_extension.dart';
|
||||||
|
|
||||||
@@ -18,9 +19,55 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
|
|
||||||
String? _selectedTeamId;
|
String? _selectedTeamId;
|
||||||
String _selectedTeamName = "Selecionar Equipa";
|
String _selectedTeamName = "Selecionar Equipa";
|
||||||
|
String? _selectedTeamLogo;
|
||||||
|
|
||||||
String _sortColumn = 'pts';
|
String _sortColumn = 'pts';
|
||||||
bool _isAscending = false;
|
bool _isAscending = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSelectedTeam();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSelectedTeam() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final savedId = prefs.getString('last_team_id');
|
||||||
|
|
||||||
|
if (savedId != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_selectedTeamId = savedId;
|
||||||
|
_selectedTeamName = prefs.getString('last_team_name') ?? "Selecionar Equipa";
|
||||||
|
_selectedTeamLogo = prefs.getString('last_team_logo');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveSelectedTeam() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (_selectedTeamId != null) {
|
||||||
|
await prefs.setString('last_team_id', _selectedTeamId!);
|
||||||
|
await prefs.setString('last_team_name', _selectedTeamName);
|
||||||
|
if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) {
|
||||||
|
await prefs.setString('last_team_logo', _selectedTeamLogo!);
|
||||||
|
} else {
|
||||||
|
await prefs.remove('last_team_logo');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
|
if (userId != null && _selectedTeamId != null) {
|
||||||
|
try {
|
||||||
|
await _supabase.from('profiles').upsert({
|
||||||
|
'id': userId,
|
||||||
|
'selected_team_id': _selectedTeamId,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Erro ao guardar equipa no Supabase: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final bgColor = Theme.of(context).cardTheme.color ?? Colors.white;
|
final bgColor = Theme.of(context).cardTheme.color ?? Colors.white;
|
||||||
@@ -44,7 +91,19 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
(_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty)
|
||||||
|
? ClipOval(
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: _selectedTeamLogo!,
|
||||||
|
width: 24 * context.sf,
|
||||||
|
height: 24 * context.sf,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
|
||||||
|
|
||||||
SizedBox(width: 10 * context.sf),
|
SizedBox(width: 10 * context.sf),
|
||||||
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
|
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
|
||||||
]),
|
]),
|
||||||
@@ -99,12 +158,11 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👇 AGORA GUARDA TAMBÉM O IMAGE_URL DO MEMBRO PARA MOSTRAR NA TABELA
|
|
||||||
List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
|
List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
|
||||||
Map<String, Map<String, dynamic>> aggregated = {};
|
Map<String, Map<String, dynamic>> aggregated = {};
|
||||||
for (var member in members) {
|
for (var member in members) {
|
||||||
String name = member['name']?.toString() ?? "Desconhecido";
|
String name = member['name']?.toString() ?? "Desconhecido";
|
||||||
String? imageUrl = member['image_url']?.toString(); // 👈 CAPTURA A IMAGEM AQUI
|
String? imageUrl = member['image_url']?.toString();
|
||||||
aggregated[name] = {'name': name, 'image_url': imageUrl, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
|
aggregated[name] = {'name': name, 'image_url': imageUrl, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
|
||||||
}
|
}
|
||||||
for (var row in stats) {
|
for (var row in stats) {
|
||||||
@@ -140,78 +198,84 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
|
|
||||||
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, Color bgColor, Color textColor) {
|
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, Color bgColor, Color textColor) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent, // 👇 VOLTOU A ESTAR TRANSPARENTE COMO TINHAS ANTES!
|
||||||
|
width: double.infinity,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: DataTable(
|
physics: const ClampingScrollPhysics(), // Mantém-se o Clamping para não puxar mais do que o ecrã
|
||||||
columnSpacing: 25 * context.sf,
|
child: ConstrainedBox(
|
||||||
headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
|
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
||||||
dataRowMaxHeight: 60 * context.sf,
|
child: DataTable(
|
||||||
dataRowMinHeight: 60 * context.sf,
|
columnSpacing: 20 * context.sf,
|
||||||
columns: [
|
horizontalMargin: 16 * context.sf,
|
||||||
DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))),
|
headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
|
||||||
_buildSortableColumn(context, 'J', 'j', textColor),
|
dataRowMaxHeight: 60 * context.sf,
|
||||||
_buildSortableColumn(context, 'PTS', 'pts', textColor),
|
dataRowMinHeight: 60 * context.sf,
|
||||||
_buildSortableColumn(context, 'AST', 'ast', textColor),
|
columns: [
|
||||||
_buildSortableColumn(context, 'RBS', 'rbs', textColor),
|
DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))),
|
||||||
_buildSortableColumn(context, 'STL', 'stl', textColor),
|
_buildSortableColumn(context, 'J', 'j', textColor),
|
||||||
_buildSortableColumn(context, 'BLK', 'blk', textColor),
|
_buildSortableColumn(context, 'PTS', 'pts', textColor),
|
||||||
_buildSortableColumn(context, 'DEF 🛡️', 'def', textColor),
|
_buildSortableColumn(context, 'AST', 'ast', textColor),
|
||||||
_buildSortableColumn(context, 'MVP 🏆', 'mvp', textColor),
|
_buildSortableColumn(context, 'RBS', 'rbs', textColor),
|
||||||
],
|
_buildSortableColumn(context, 'STL', 'stl', textColor),
|
||||||
rows: [
|
_buildSortableColumn(context, 'BLK', 'blk', textColor),
|
||||||
...players.map((player) => DataRow(cells: [
|
_buildSortableColumn(context, 'DEF 🛡️', 'def', textColor),
|
||||||
DataCell(
|
_buildSortableColumn(context, 'MVP 🏆', 'mvp', textColor),
|
||||||
Row(
|
],
|
||||||
children: [
|
rows: [
|
||||||
// 👇 FOTO DO JOGADOR NA TABELA (COM CACHE!) 👇
|
...players.map((player) => DataRow(cells: [
|
||||||
ClipOval(
|
DataCell(
|
||||||
child: Container(
|
Row(
|
||||||
width: 30 * context.sf,
|
children: [
|
||||||
height: 30 * context.sf,
|
ClipOval(
|
||||||
color: Colors.grey.withOpacity(0.2),
|
child: Container(
|
||||||
child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty)
|
width: 30 * context.sf,
|
||||||
? CachedNetworkImage(
|
height: 30 * context.sf,
|
||||||
imageUrl: player['image_url'],
|
color: Colors.grey.withOpacity(0.2),
|
||||||
fit: BoxFit.cover,
|
child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty)
|
||||||
fadeInDuration: Duration.zero,
|
? CachedNetworkImage(
|
||||||
placeholder: (context, url) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
imageUrl: player['image_url'],
|
||||||
errorWidget: (context, url, error) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
fit: BoxFit.cover,
|
||||||
)
|
fadeInDuration: Duration.zero,
|
||||||
: Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
placeholder: (context, url) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
||||||
|
)
|
||||||
|
: Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(width: 10 * context.sf),
|
||||||
SizedBox(width: 10 * context.sf),
|
Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor))
|
||||||
Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor))
|
]
|
||||||
]
|
)
|
||||||
)
|
),
|
||||||
),
|
DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))),
|
||||||
DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))),
|
_buildStatCell(context, player['pts'], textColor, isHighlight: true),
|
||||||
_buildStatCell(context, player['pts'], textColor, isHighlight: true),
|
_buildStatCell(context, player['ast'], textColor),
|
||||||
_buildStatCell(context, player['ast'], textColor),
|
_buildStatCell(context, player['rbs'], textColor),
|
||||||
_buildStatCell(context, player['rbs'], textColor),
|
_buildStatCell(context, player['stl'], textColor),
|
||||||
_buildStatCell(context, player['stl'], textColor),
|
_buildStatCell(context, player['blk'], textColor),
|
||||||
_buildStatCell(context, player['blk'], textColor),
|
_buildStatCell(context, player['def'], textColor, isBlue: true),
|
||||||
_buildStatCell(context, player['def'], textColor, isBlue: true),
|
_buildStatCell(context, player['mvp'], textColor, isGold: true),
|
||||||
_buildStatCell(context, player['mvp'], textColor, isGold: true),
|
])),
|
||||||
])),
|
DataRow(
|
||||||
DataRow(
|
color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)),
|
||||||
color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)),
|
cells: [
|
||||||
cells: [
|
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: textColor, fontSize: 12 * context.sf))),
|
||||||
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: textColor, fontSize: 12 * context.sf))),
|
DataCell(Center(child: Text(teamTotals['j'].toString(), style: TextStyle(fontWeight: FontWeight.bold, color: textColor)))),
|
||||||
DataCell(Center(child: Text(teamTotals['j'].toString(), style: TextStyle(fontWeight: FontWeight.bold, color: textColor)))),
|
_buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true),
|
||||||
_buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true),
|
_buildStatCell(context, teamTotals['ast'], textColor),
|
||||||
_buildStatCell(context, teamTotals['ast'], textColor),
|
_buildStatCell(context, teamTotals['rbs'], textColor),
|
||||||
_buildStatCell(context, teamTotals['rbs'], textColor),
|
_buildStatCell(context, teamTotals['stl'], textColor),
|
||||||
_buildStatCell(context, teamTotals['stl'], textColor),
|
_buildStatCell(context, teamTotals['blk'], textColor),
|
||||||
_buildStatCell(context, teamTotals['blk'], textColor),
|
_buildStatCell(context, teamTotals['def'], textColor, isBlue: true),
|
||||||
_buildStatCell(context, teamTotals['def'], textColor, isBlue: true),
|
_buildStatCell(context, teamTotals['mvp'], textColor, isGold: true),
|
||||||
_buildStatCell(context, teamTotals['mvp'], textColor, isGold: true),
|
]
|
||||||
]
|
)
|
||||||
)
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -247,10 +311,40 @@ class _StatusPageState extends State<StatusPage> {
|
|||||||
stream: _teamController.teamsStream,
|
stream: _teamController.teamsStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final teams = snapshot.data ?? [];
|
final teams = snapshot.data ?? [];
|
||||||
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile(
|
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) {
|
||||||
title: Text(teams[i]['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
final team = teams[i];
|
||||||
onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); },
|
final logoUrl = team['image_url'];
|
||||||
));
|
|
||||||
|
return ListTile(
|
||||||
|
leading: ClipOval(
|
||||||
|
child: Container(
|
||||||
|
width: 36 * context.sf,
|
||||||
|
height: 36 * context.sf,
|
||||||
|
color: AppTheme.primaryRed.withOpacity(0.1),
|
||||||
|
child: (logoUrl != null && logoUrl.isNotEmpty)
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: logoUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
|
||||||
|
errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
|
||||||
|
)
|
||||||
|
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
||||||
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
_selectedTeamId = team['id'].toString();
|
||||||
|
_selectedTeamName = team['name'];
|
||||||
|
_selectedTeamLogo = logoUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
await _saveSelectedTeam();
|
||||||
|
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import app_links
|
|||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import printing
|
import printing
|
||||||
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
@@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||||
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
|||||||
126
pubspec.lock
126
pubspec.lock
@@ -45,10 +45,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.9"
|
version: "3.6.1"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -121,6 +121,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
code_assets:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_assets
|
||||||
|
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -177,6 +185,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.1"
|
version: "0.3.1"
|
||||||
|
equatable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.8"
|
||||||
|
excel:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: excel
|
||||||
|
sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.6"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -189,10 +213,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: ffi
|
name: ffi
|
||||||
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.2.0"
|
||||||
|
ffi_leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi_leak_tracker
|
||||||
|
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.2"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -288,6 +320,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.0"
|
version: "2.5.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
gotrue:
|
gotrue:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -304,6 +344,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -324,10 +372,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.8.0"
|
version: "4.3.0"
|
||||||
image_cropper:
|
image_cropper:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -496,6 +544,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
native_toolchain_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: native_toolchain_c
|
||||||
|
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.17.6"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -624,14 +680,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
posix:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: posix
|
|
||||||
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.5.0"
|
|
||||||
postgrest:
|
postgrest:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -656,6 +704,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5+1"
|
version: "6.1.5+1"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
qr:
|
qr:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -672,6 +728,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.0"
|
version: "2.7.0"
|
||||||
|
record_use:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_use
|
||||||
|
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
retry:
|
retry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -688,6 +752,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.28.0"
|
version: "0.28.0"
|
||||||
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
sha256: a857d8b1479250aff6b57a51b2c02d31ca05848d441817c43f1640c885c286c0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "13.1.0"
|
||||||
|
share_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
sha256: "7f7ae28cf400d13f811e297ff37742dba83b79e0a6f5dce14eec0248274e6ce9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.1.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -997,6 +1077,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: ba7d5750e3441caa1bbe31d9e516348fcf8dfcb32aa29ef87a844a59f4d1f1d0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1013,6 +1101,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.6.1"
|
version: "6.6.1"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
yet_another_json_isolate:
|
yet_another_json_isolate:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1023,4 +1119,4 @@ packages:
|
|||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.0 <4.0.0"
|
dart: ">=3.10.0 <4.0.0"
|
||||||
flutter: ">=3.38.0"
|
flutter: ">=3.38.1"
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ dependencies:
|
|||||||
shared_preferences: ^2.5.4
|
shared_preferences: ^2.5.4
|
||||||
printing: ^5.14.3
|
printing: ^5.14.3
|
||||||
pdf: ^3.12.0
|
pdf: ^3.12.0
|
||||||
|
excel: ^4.0.6
|
||||||
|
share_plus: ^13.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <app_links/app_links_plugin_c_api.h>
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <printing/printing_plugin.h>
|
#include <printing/printing_plugin.h>
|
||||||
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
@@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
PrintingPluginRegisterWithRegistrar(
|
PrintingPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||||
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
app_links
|
app_links
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
printing
|
printing
|
||||||
|
share_plus
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user