This commit is contained in:
2026-05-15 12:43:30 +01:00
parent 1e38c4ad57
commit 332361c296
7 changed files with 1545 additions and 967 deletions

157
SYNC_CHANGES_SUMMARY.md Normal file
View File

@@ -0,0 +1,157 @@
# Resumo das Mudanças - Sincronização de Jogo em Tempo Real
## 1. lib/controllers/placar_controller.dart
### Adicionado ao constructor:
```dart
final void Function(String actionType, Map<String, dynamic> actionData)? onSyncAction;
PlacarController({
required this.gameId,
required this.myTeam,
required this.opponentTeam,
this.onSyncAction, // ← NOVO
});
```
### Adicionado método _dispatchSyncAction:
```dart
void _dispatchSyncAction(String actionType, Map<String, dynamic> actionData) {
if (onSyncAction != null) {
final enrichedActionData = Map<String, dynamic>.from(actionData)
..['remaining_seconds'] = durationNotifier.value.inSeconds
..['is_running'] = isRunning;
onSyncAction!(actionType, enrichedActionData);
}
}
```
### Adicionado em 5 métodos (chamada _dispatchSyncAction):
- `useTimeout()` → dispatch `'use_timeout'`
- `handleSubbing()` → dispatch `'subbing'`
- `swapCourtPlayers()` → dispatch `'swap_players'`
- `registerFoul()` → dispatch `'register_foul'`
- `commitStat()` → dispatch `'commit_stat'`
**Exemplo em commitStat:**
```dart
_dispatchSyncAction('commit_stat', {
'action': action,
'player_data': playerData,
});
```
---
## 2. lib/pages/PlacarPage.dart
### Adicionado ao state:
```dart
String? _lastAppliedSyncEventId; // ← NOVO - deduplicação de eventos
```
### Constructor do controller:
```dart
_controller = PlacarController(
gameId: widget.gameId,
myTeam: widget.myTeam,
opponentTeam: widget.opponentTeam,
onSyncAction: _onLocalControllerSync, // ← CONECTADO
);
```
### Adicionado novo método _onLocalControllerSync:
```dart
void _onLocalControllerSync(String actionType, Map<String, dynamic> actionData) {
if (_sessionId == null || _isApplyingRemoteSync) return;
print("📤 Enviando sync action local: $actionType -> $actionData");
_sharingController.sendSyncEvent(_sessionId!, actionType, actionData);
}
```
### Atualizado _setupSyncListener (deduplicação):
```dart
_syncSubscription = _sharingController.listenToGameSyncOthers(_sessionId!).listen(
(dynamic event) {
Map<String, dynamic>? record;
if (event is List && event.isNotEmpty) {
for (final item in event) {
final row = item as Map<String, dynamic>?;
if (row == null) continue;
final rowId = row['id']?.toString();
if (rowId != null && rowId != _lastAppliedSyncEventId) {
record = row;
break; // ← para no primeiro evento novo
}
}
} else if (event is Map<String, dynamic>) {
record = Map<String, dynamic>.from(event);
}
if (record != null) {
final recordId = record['id']?.toString();
if (recordId != null && recordId == _lastAppliedSyncEventId) return;
if (recordId != null) _lastAppliedSyncEventId = recordId;
_applyRemoteSyncEvent(record);
}
},
);
```
### Atualizado _applyRemoteSyncEvent (aplicar estado remoto):
```dart
void _applyRemoteSyncEvent(Map<String, dynamic> record) {
final actionType = record['action_type']?.toString();
final actionData = Map<String, dynamic>.from(record['action_data'] ?? {});
// ← NOVO: aplicar timer remotamente em TODAS as ações
final remoteSeconds = int.tryParse(actionData['remaining_seconds']?.toString() ?? '');
final remoteIsRunning = actionData['is_running'] == true;
if (remoteSeconds != null) {
_controller.durationNotifier.value = Duration(seconds: remoteSeconds);
}
if (remoteIsRunning != _controller.isRunning) {
_isApplyingRemoteSync = true;
_controller.toggleTimer(context);
_isApplyingRemoteSync = false;
}
// Aplicar ações específicas
if (actionType == 'toggle_timer') {
setState(() {});
} else if (actionType == 'commit_stat') {
// aplicar pontos/faltas
} else if (actionType == 'register_foul') {
// aplicar falta
} else if (actionType == 'subbing') {
// aplicar substituição
} else if (actionType == 'swap_players') {
// trocar posição
} else if (actionType == 'use_timeout') {
// usar timeout
}
}
```
---
## Fluxo Completo
1. **Ação Local**`commitStat()` no controller
2. **Controller emite**`_dispatchSyncAction('commit_stat', {action, player_data, remaining_seconds, is_running})`
3. **PlacarPage escuta**`_onLocalControllerSync()` recebe o evento
4. **Envia ao Supabase**`sendSyncEvent()` armazena em `game_sync_events`
5. **Parceiro recebe**`listenToGameSyncOthers()` retorna o evento
6. **Aplica remotamente**`_applyRemoteSyncEvent()` executa a ação no parceiro
7. **Estado sincronizado** → Ambos têm timer, pontos, faltas idênticos
---
## Resultado
✅ Timer não reseta ao marcar ponto
✅ Pontos sincronizam entre os dois lados
✅ Faltas sincronizam
✅ Timeouts sincronizam
✅ Substituições sincronizam
✅ Posições de jogadores sincronizam

BIN
assets/campone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 MiB

View File

@@ -1,3 +1,4 @@
// ...existing code...
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'dart:math'; import 'dart:math';
@@ -81,14 +82,18 @@ class GameSharingController {
.from('profiles') .from('profiles')
.select('username, full_name') .select('username, full_name')
.eq('id', createdBy) .eq('id', createdBy)
.single(); .maybeSingle();
print("👤 Criador: ${creatorData['full_name'] ?? creatorData['username']}"); final creatorName = creatorData != null
? (creatorData['full_name'] ?? creatorData['username'] ?? 'Utilizador')
: 'Utilizador';
print("👤 Criador: $creatorName");
return { return {
'session_id': session['id'], 'session_id': session['id'],
'game_id': gameId, 'game_id': gameId,
'creator_name': creatorData['full_name'] ?? creatorData['username'] ?? 'Utilizador', 'creator_name': creatorName,
'game': gameData, 'game': gameData,
}; };
} catch (e) { } catch (e) {
@@ -156,14 +161,16 @@ class GameSharingController {
Future<bool> sendSyncEvent( Future<bool> sendSyncEvent(
String sessionId, String sessionId,
String actionType, String actionType,
Map<String, dynamic> actionData, Map<String, dynamic> actionData, {
) async { String? playerId, // opcional: identifica jogador/entidade alvo
}) async {
try { try {
await _supabase.from('game_sync_events').insert({ await _supabase.from('game_sync_events').insert({
'session_id': sessionId, 'session_id': sessionId,
'action_type': actionType, 'action_type': actionType,
'action_data': actionData, 'action_data': actionData,
'triggered_by': myUserId, 'triggered_by': myUserId,
if (playerId != null) 'player_id': playerId,
}); });
print("✅ Evento sincronizado: $actionType"); print("✅ Evento sincronizado: $actionType");
@@ -186,6 +193,24 @@ class GameSharingController {
.order('created_at', ascending: false); .order('created_at', ascending: false);
} }
/// Retorna apenas os eventos que NÃO foram disparados pelo utilizador atual.
/// Emite uma lista de eventos (List<Map<String, dynamic>>) por cada atualização.
Stream<List<Map<String, dynamic>>> listenToGameSyncOthers(String sessionId) {
return listenToGameSync(sessionId).map((data) {
List<Map<String, dynamic>> rows = [];
try {
if (data is List) {
rows = List<Map<String, dynamic>>.from(data);
} else if (data is Map) {
rows = [Map<String, dynamic>.from(data)];
}
} catch (_) {
return <Map<String, dynamic>>[];
}
return rows.where((r) => (r['triggered_by'] as String?) != myUserId).toList();
});
}
// ==================================== // ====================================
// 6⃣ OBTER ÚLTIMOS EVENTOS // 6⃣ OBTER ÚLTIMOS EVENTOS
// ==================================== // ====================================

View File

@@ -25,13 +25,23 @@ class ShotRecord {
}); });
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'relativeX': relativeX, 'relativeY': relativeY, 'isMake': isMake, 'relativeX': relativeX,
'playerId': playerId, 'playerName': playerName, 'zone': zone, 'points': points, 'relativeY': relativeY,
'isMake': isMake,
'playerId': playerId,
'playerName': playerName,
'zone': zone,
'points': points,
}; };
factory ShotRecord.fromJson(Map<String, dynamic> json) => ShotRecord( factory ShotRecord.fromJson(Map<String, dynamic> json) => ShotRecord(
relativeX: json['relativeX'], relativeY: json['relativeY'], isMake: json['isMake'], relativeX: json['relativeX'],
playerId: json['playerId'], playerName: json['playerName'], zone: json['zone'], points: json['points'], relativeY: json['relativeY'],
isMake: json['isMake'],
playerId: json['playerId'],
playerName: json['playerName'],
zone: json['zone'],
points: json['points'],
); );
} }
@@ -39,13 +49,96 @@ class PlacarController extends ChangeNotifier {
final String gameId; final String gameId;
final String myTeam; final String myTeam;
final String opponentTeam; final String opponentTeam;
final void Function(String actionType, Map<String, dynamic> actionData)?
onSyncAction;
PlacarController({ PlacarController({
required this.gameId, required this.gameId,
required this.myTeam, required this.myTeam,
required this.opponentTeam, required this.opponentTeam,
this.onSyncAction,
}); });
void _dispatchSyncAction(String actionType, Map<String, dynamic> actionData) {
if (onSyncAction != null) {
final enrichedActionData = Map<String, dynamic>.from(actionData)
..['remaining_seconds'] = durationNotifier.value.inSeconds
..['is_running'] = isRunning
..['current_quarter'] = currentQuarter
..['my_fouls'] = myFouls
..['opponent_fouls'] = opponentFouls
..['my_timeouts_used'] = myTimeoutsUsed
..['opponent_timeouts_used'] = opponentTimeoutsUsed;
onSyncAction!(actionType, enrichedActionData);
}
}
void _startTimer() {
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!isRunning) return;
if (durationNotifier.value.inSeconds > 0) {
void addTimeToCourt(List<String> court) {
for (String id in court) {
if (playerStats.containsKey(id)) {
int currentSec = playerStats[id]!['sec'] ?? 0;
playerStats[id]!['sec'] = currentSec + 1;
playerStats[id]!['min'] = (currentSec + 1) ~/ 60;
}
}
}
addTimeToCourt(myCourt);
addTimeToCourt(oppCourt);
durationNotifier.value -= const Duration(seconds: 1);
} else {
timer.cancel();
isRunning = false;
if (currentQuarter < 4) {
currentQuarter++;
durationNotifier.value = const Duration(minutes: 10);
myFouls = 0;
opponentFouls = 0;
myTimeoutsUsed = 0;
opponentTimeoutsUsed = 0;
_scheduleAutoSave();
}
notifyListeners();
_dispatchSyncAction('period_ended', {});
}
});
}
void _setTimerRunning(bool shouldRun, {bool emitSync = true}) {
print("🔧 _setTimerRunning: shouldRun=$shouldRun, isRunning=$isRunning");
if (shouldRun == isRunning) {
print("🔧 Guardado: shouldRun == isRunning");
return;
}
isRunning = shouldRun;
if (!shouldRun) {
print("🛑 Cancelando timer");
timer?.cancel();
_scheduleAutoSave();
} else {
print("▶️ Iniciando timer");
_startTimer();
}
notifyListeners();
print("✅ notifyListeners chamado");
if (emitSync) {
print("📡 Despachando sync action");
_dispatchSyncAction('toggle_timer', {'is_running': isRunning});
}
}
void applyRemoteTimerState(bool shouldRun) {
_setTimerRunning(shouldRun, emitSync: false);
}
bool isLoading = true; bool isLoading = true;
bool isSaving = false; bool isSaving = false;
bool gameWasAlreadyFinished = false; bool gameWasAlreadyFinished = false;
@@ -80,7 +173,9 @@ class PlacarController extends ChangeNotifier {
List<String> playByPlay = []; List<String> playByPlay = [];
ValueNotifier<Duration> durationNotifier = ValueNotifier(const Duration(minutes: 10)); ValueNotifier<Duration> durationNotifier = ValueNotifier(
const Duration(minutes: 10),
);
Timer? timer; Timer? timer;
bool isRunning = false; bool isRunning = false;
@@ -96,21 +191,41 @@ class PlacarController extends ChangeNotifier {
try { try {
await Future.delayed(const Duration(milliseconds: 1500)); await Future.delayed(const Duration(milliseconds: 1500));
myCourt.clear(); myBench.clear(); oppCourt.clear(); oppBench.clear(); myCourt.clear();
playerNames.clear(); playerStats.clear(); playerNumbers.clear(); myBench.clear();
matchShots.clear(); playByPlay.clear(); myFouls = 0; opponentFouls = 0; oppCourt.clear();
oppBench.clear();
playerNames.clear();
playerStats.clear();
playerNumbers.clear();
matchShots.clear();
playByPlay.clear();
myFouls = 0;
opponentFouls = 0;
final gameResponse = await supabase.from('games').select().eq('id', gameId).single(); final gameResponse = await supabase
.from('games')
.select()
.eq('id', gameId)
.single();
myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0; myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0;
opponentScore = int.tryParse(gameResponse['opponent_score']?.toString() ?? '0') ?? 0; opponentScore =
int.tryParse(gameResponse['opponent_score']?.toString() ?? '0') ?? 0;
int totalSeconds = int.tryParse(gameResponse['remaining_seconds']?.toString() ?? '600') ?? 600; int totalSeconds =
int.tryParse(
gameResponse['remaining_seconds']?.toString() ?? '600',
) ??
600;
durationNotifier.value = Duration(seconds: totalSeconds); durationNotifier.value = Duration(seconds: totalSeconds);
myTimeoutsUsed = int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0; myTimeoutsUsed =
opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0; int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0;
currentQuarter = int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1; opponentTimeoutsUsed =
int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
currentQuarter =
int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1;
gameWasAlreadyFinished = gameResponse['status'] == 'Terminado'; gameWasAlreadyFinished = gameResponse['status'] == 'Terminado';
@@ -120,25 +235,49 @@ class PlacarController extends ChangeNotifier {
playByPlay = []; playByPlay = [];
} }
final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]); final teamsResponse = await supabase
.from('teams')
.select('id, name')
.inFilter('name', [myTeam, opponentTeam]);
for (var t in teamsResponse) { for (var t in teamsResponse) {
if (t['name'] == myTeam) myTeamDbId = t['id']; if (t['name'] == myTeam) myTeamDbId = t['id'];
if (t['name'] == opponentTeam) oppTeamDbId = t['id']; if (t['name'] == opponentTeam) oppTeamDbId = t['id'];
} }
List<dynamic> myPlayers = myTeamDbId != null ? await supabase.from('members').select().eq('team_id', myTeamDbId!).eq('type', 'Jogador') : []; List<dynamic> myPlayers = myTeamDbId != null
List<dynamic> oppPlayers = oppTeamDbId != null ? await supabase.from('members').select().eq('team_id', oppTeamDbId!).eq('type', 'Jogador') : []; ? await supabase
.from('members')
.select()
.eq('team_id', myTeamDbId!)
.eq('type', 'Jogador')
: [];
List<dynamic> oppPlayers = oppTeamDbId != null
? await supabase
.from('members')
.select()
.eq('team_id', oppTeamDbId!)
.eq('type', 'Jogador')
: [];
final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId); final statsResponse = await supabase
.from('player_stats')
.select()
.eq('game_id', gameId);
final Map<String, dynamic> savedStats = { final Map<String, dynamic> savedStats = {
for (var item in statsResponse) item['member_id'].toString(): item for (var item in statsResponse) item['member_id'].toString(): item,
}; };
for (int i = 0; i < myPlayers.length; i++) { for (int i = 0; i < myPlayers.length; i++) {
String dbId = myPlayers[i]['id'].toString(); String dbId = myPlayers[i]['id'].toString();
String name = myPlayers[i]['name'].toString(); String name = myPlayers[i]['name'].toString();
_registerPlayer(name: name, number: myPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: true, isCourt: i < 5); _registerPlayer(
name: name,
number: myPlayers[i]['number']?.toString() ?? "0",
dbId: dbId,
isMyTeam: true,
isCourt: i < 5,
);
if (savedStats.containsKey(dbId)) { if (savedStats.containsKey(dbId)) {
var s = savedStats[dbId]; var s = savedStats[dbId];
@@ -152,7 +291,13 @@ class PlacarController extends ChangeNotifier {
String dbId = oppPlayers[i]['id'].toString(); String dbId = oppPlayers[i]['id'].toString();
String name = oppPlayers[i]['name'].toString(); String name = oppPlayers[i]['name'].toString();
_registerPlayer(name: name, number: oppPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: false, isCourt: i < 5); _registerPlayer(
name: name,
number: oppPlayers[i]['number']?.toString() ?? "0",
dbId: dbId,
isMyTeam: false,
isCourt: i < 5,
);
if (savedStats.containsKey(dbId)) { if (savedStats.containsKey(dbId)) {
var s = savedStats[dbId]; var s = savedStats[dbId];
@@ -162,17 +307,24 @@ class PlacarController extends ChangeNotifier {
} }
_padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false); _padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false);
final shotsResponse = await supabase.from('shot_locations').select().eq('game_id', gameId); final shotsResponse = await supabase
.from('shot_locations')
.select()
.eq('game_id', gameId);
for (var shotData in shotsResponse) { for (var shotData in shotsResponse) {
matchShots.add(ShotRecord( matchShots.add(
relativeX: double.parse(shotData['relative_x'].toString()), ShotRecord(
relativeY: double.parse(shotData['relative_y'].toString()), relativeX: double.parse(shotData['relative_x'].toString()),
isMake: shotData['is_make'] == true, relativeY: double.parse(shotData['relative_y'].toString()),
playerId: shotData['member_id'].toString(), isMake: shotData['is_make'] == true,
playerName: shotData['player_name'].toString(), playerId: shotData['member_id'].toString(),
zone: shotData['zone']?.toString(), playerName: shotData['player_name'].toString(),
points: shotData['points'] != null ? int.parse(shotData['points'].toString()) : null, zone: shotData['zone']?.toString(),
)); points: shotData['points'] != null
? int.parse(shotData['points'].toString())
: null,
),
);
} }
await _loadLocalBackup(); await _loadLocalBackup();
@@ -188,42 +340,103 @@ class PlacarController extends ChangeNotifier {
void _loadSavedPlayerStats(String dbId, Map<String, dynamic> s) { void _loadSavedPlayerStats(String dbId, Map<String, dynamic> s) {
playerStats[dbId] = { playerStats[dbId] = {
"pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0, "pts": s['pts'] ?? 0,
"stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0, "rbs": s['rbs'] ?? 0,
"fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0, "ast": s['ast'] ?? 0,
"ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0, "stl": s['stl'] ?? 0,
"p2m": s['p2m'] ?? 0, "p2a": s['p2a'] ?? 0, "p3m": s['p3m'] ?? 0, "p3a": s['p3a'] ?? 0, "tov": s['tov'] ?? 0,
"so": s['so'] ?? 0, "il": s['il'] ?? 0, "li": s['li'] ?? 0, "blk": s['blk'] ?? 0,
"pa": s['pa'] ?? 0, "tres_seg": s['tres_seg'] ?? 0, "dr": s['dr'] ?? 0, "fls": s['fls'] ?? 0,
"fgm": s['fgm'] ?? 0,
"fga": s['fga'] ?? 0,
"ftm": s['ftm'] ?? 0,
"fta": s['fta'] ?? 0,
"orb": s['orb'] ?? 0,
"drb": s['drb'] ?? 0,
"p2m": s['p2m'] ?? 0,
"p2a": s['p2a'] ?? 0,
"p3m": s['p3m'] ?? 0,
"p3a": s['p3a'] ?? 0,
"so": s['so'] ?? 0,
"il": s['il'] ?? 0,
"li": s['li'] ?? 0,
"pa": s['pa'] ?? 0,
"tres_seg": s['tres_seg'] ?? 0,
"dr": s['dr'] ?? 0,
"min": (s['minutos_jogados'] ?? 0) ~/ 60, "min": (s['minutos_jogados'] ?? 0) ~/ 60,
"sec": s['minutos_jogados'] ?? 0, "sec": s['minutos_jogados'] ?? 0,
}; };
} }
void _registerPlayer({required String name, required String number, String? dbId, required bool isMyTeam, required bool isCourt}) { void _registerPlayer({
String id = dbId ?? "fake_${DateTime.now().millisecondsSinceEpoch}_${math.Random().nextInt(9999)}"; required String name,
required String number,
String? dbId,
required bool isMyTeam,
required bool isCourt,
}) {
String id =
dbId ??
"fake_${DateTime.now().millisecondsSinceEpoch}_${math.Random().nextInt(9999)}";
playerNames[id] = name; playerNames[id] = name;
playerNumbers[id] = number; playerNumbers[id] = number;
playerStats[id] = { playerStats[id] = {
"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "pts": 0,
"fls": 0, "fgm": 0, "fga": 0, "ftm": 0, "fta": 0, "orb": 0, "drb": 0, "rbs": 0,
"p2m": 0, "p2a": 0, "p3m": 0, "p3a": 0, "ast": 0,
"so": 0, "il": 0, "li": 0, "pa": 0, "tres_seg": 0, "dr": 0, "stl": 0,
"min": 0, "sec": 0 "tov": 0,
"blk": 0,
"fls": 0,
"fgm": 0,
"fga": 0,
"ftm": 0,
"fta": 0,
"orb": 0,
"drb": 0,
"p2m": 0,
"p2a": 0,
"p3m": 0,
"p3a": 0,
"so": 0,
"il": 0,
"li": 0,
"pa": 0,
"tres_seg": 0,
"dr": 0,
"min": 0,
"sec": 0,
}; };
if (isMyTeam) { if (isMyTeam) {
if (isCourt) myCourt.add(id); else myBench.add(id); if (isCourt)
myCourt.add(id);
else
myBench.add(id);
} else { } else {
if (isCourt) oppCourt.add(id); else oppBench.add(id); if (isCourt)
oppCourt.add(id);
else
oppBench.add(id);
} }
} }
void _padTeam(List<String> court, List<String> bench, String prefix, {required bool isMyTeam}) { void _padTeam(
List<String> court,
List<String> bench,
String prefix, {
required bool isMyTeam,
}) {
while (court.length < 5) { while (court.length < 5) {
_registerPlayer(name: "Sem $prefix ${court.length + 1}", number: "0", dbId: null, isMyTeam: isMyTeam, isCourt: true); _registerPlayer(
name: "Sem $prefix ${court.length + 1}",
number: "0",
dbId: null,
isMyTeam: isMyTeam,
isCourt: true,
);
} }
} }
@@ -238,12 +451,19 @@ class PlacarController extends ChangeNotifier {
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final backupData = { final backupData = {
'myScore': myScore, 'opponentScore': opponentScore, 'myScore': myScore,
'myFouls': myFouls, 'opponentFouls': opponentFouls, 'opponentScore': opponentScore,
'currentQuarter': currentQuarter, 'duration': durationNotifier.value.inSeconds, 'myFouls': myFouls,
'myTimeoutsUsed': myTimeoutsUsed, 'opponentTimeoutsUsed': opponentTimeoutsUsed, 'opponentFouls': opponentFouls,
'currentQuarter': currentQuarter,
'duration': durationNotifier.value.inSeconds,
'myTimeoutsUsed': myTimeoutsUsed,
'opponentTimeoutsUsed': opponentTimeoutsUsed,
'playerStats': playerStats, 'playerStats': playerStats,
'myCourt': myCourt, 'myBench': myBench, 'oppCourt': oppCourt, 'oppBench': oppBench, 'myCourt': myCourt,
'myBench': myBench,
'oppCourt': oppCourt,
'oppBench': oppBench,
'matchShots': matchShots.map((s) => s.toJson()).toList(), 'matchShots': matchShots.map((s) => s.toJson()).toList(),
'playByPlay': playByPlay, 'playByPlay': playByPlay,
}; };
@@ -261,16 +481,24 @@ class PlacarController extends ChangeNotifier {
if (backupString != null) { if (backupString != null) {
final data = jsonDecode(backupString); final data = jsonDecode(backupString);
myScore = data['myScore']; opponentScore = data['opponentScore']; myScore = data['myScore'];
myFouls = data['myFouls']; opponentFouls = data['opponentFouls']; opponentScore = data['opponentScore'];
currentQuarter = data['currentQuarter']; durationNotifier.value = Duration(seconds: data['duration']); myFouls = data['myFouls'];
myTimeoutsUsed = data['myTimeoutsUsed']; opponentTimeoutsUsed = data['opponentTimeoutsUsed']; opponentFouls = data['opponentFouls'];
currentQuarter = data['currentQuarter'];
durationNotifier.value = Duration(seconds: data['duration']);
myTimeoutsUsed = data['myTimeoutsUsed'];
opponentTimeoutsUsed = data['opponentTimeoutsUsed'];
myCourt = List<String>.from(data['myCourt']); myBench = List<String>.from(data['myBench']); myCourt = List<String>.from(data['myCourt']);
oppCourt = List<String>.from(data['oppCourt']); oppBench = List<String>.from(data['oppBench']); myBench = List<String>.from(data['myBench']);
oppCourt = List<String>.from(data['oppCourt']);
oppBench = List<String>.from(data['oppBench']);
Map<String, dynamic> decodedStats = data['playerStats']; Map<String, dynamic> decodedStats = data['playerStats'];
playerStats = decodedStats.map((k, v) => MapEntry(k, Map<String, int>.from(v))); playerStats = decodedStats.map(
(k, v) => MapEntry(k, Map<String, int>.from(v)),
);
List<dynamic> decodedShots = data['matchShots']; List<dynamic> decodedShots = data['matchShots'];
matchShots = decodedShots.map((s) => ShotRecord.fromJson(s)).toList(); matchShots = decodedShots.map((s) => ShotRecord.fromJson(s)).toList();
@@ -283,43 +511,8 @@ class PlacarController extends ChangeNotifier {
} }
void toggleTimer(BuildContext context) { void toggleTimer(BuildContext context) {
if (isRunning) { print("⏱️ toggleTimer chamado: isRunning=$isRunning");
timer?.cancel(); _setTimerRunning(!isRunning);
_scheduleAutoSave();
} else {
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (durationNotifier.value.inSeconds > 0) {
void addTimeToCourt(List<String> court) {
for (String id in court) {
if (playerStats.containsKey(id)) {
int currentSec = playerStats[id]!["sec"] ?? 0;
playerStats[id]!["sec"] = currentSec + 1;
playerStats[id]!["min"] = (currentSec + 1) ~/ 60;
}
}
}
addTimeToCourt(myCourt);
addTimeToCourt(oppCourt);
durationNotifier.value -= const Duration(seconds: 1);
} else {
timer.cancel();
isRunning = false;
if (currentQuarter < 4) {
currentQuarter++;
durationNotifier.value = const Duration(minutes: 10);
myFouls = 0; opponentFouls = 0;
myTimeoutsUsed = 0; opponentTimeoutsUsed = 0;
_scheduleAutoSave();
}
notifyListeners();
}
});
}
isRunning = !isRunning;
notifyListeners();
} }
void useTimeout(bool isOpponent) { void useTimeout(bool isOpponent) {
@@ -332,19 +525,34 @@ class PlacarController extends ChangeNotifier {
timer?.cancel(); timer?.cancel();
_scheduleAutoSave(); _scheduleAutoSave();
notifyListeners(); notifyListeners();
_dispatchSyncAction('use_timeout', {'is_opponent': isOpponent});
} }
void handleActionDrag(BuildContext context, String action, String playerData) { void handleActionDrag(
String playerId = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); BuildContext context,
String action,
String playerData,
) {
String playerId = playerData
.replaceAll("player_my_", "")
.replaceAll("player_opp_", "");
final stats = playerStats[playerId]!; final stats = playerStats[playerId]!;
final name = playerNames[playerId]!; final name = playerNames[playerId]!;
if (stats["fls"]! >= 5 && action != "sub_foul") { if (stats["fls"]! >= 5 && action != "sub_foul") {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $name atingiu 5 faltas e está expulso!'), backgroundColor: Colors.red)); ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('🛑 $name atingiu 5 faltas e está expulso!'),
backgroundColor: Colors.red,
),
);
return; return;
} }
if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") { if (action == "add_pts_2" ||
action == "add_pts_3" ||
action == "miss_2" ||
action == "miss_3") {
pendingAction = action; pendingAction = action;
pendingPlayerId = playerData; pendingPlayerId = playerData;
isSelectingShotLocation = true; isSelectingShotLocation = true;
@@ -354,7 +562,12 @@ class PlacarController extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void handleSubbing(BuildContext context, String action, String courtPlayerId, bool isOpponent) { void handleSubbing(
BuildContext context,
String action,
String courtPlayerId,
bool isOpponent,
) {
if (action.startsWith("bench_my_") && !isOpponent) { if (action.startsWith("bench_my_") && !isOpponent) {
String benchPlayerId = action.replaceAll("bench_my_", ""); String benchPlayerId = action.replaceAll("bench_my_", "");
if (playerStats[benchPlayerId]!["fls"]! >= 5) return; if (playerStats[benchPlayerId]!["fls"]! >= 5) return;
@@ -375,12 +588,18 @@ class PlacarController extends ChangeNotifier {
} }
_scheduleAutoSave(); _scheduleAutoSave();
notifyListeners(); notifyListeners();
_dispatchSyncAction('subbing', {
'action': action,
'court_player': courtPlayerId,
'is_opponent': isOpponent,
});
} }
// ── TROCAR JOGADORES NO CAMPO ────────────────────────────────────────────── // ── TROCAR JOGADORES NO CAMPO ──────────────────────────────────────────────
void swapCourtPlayers(String draggedPlayerData, String targetPlayerData) { void swapCourtPlayers(String draggedPlayerData, String targetPlayerData) {
// Verifica se são da mesma equipa (Minha Equipa) // Verifica se são da mesma equipa (Minha Equipa)
if (draggedPlayerData.startsWith("player_my_") && targetPlayerData.startsWith("player_my_")) { if (draggedPlayerData.startsWith("player_my_") &&
targetPlayerData.startsWith("player_my_")) {
String id1 = draggedPlayerData.replaceAll("player_my_", ""); String id1 = draggedPlayerData.replaceAll("player_my_", "");
String id2 = targetPlayerData.replaceAll("player_my_", ""); String id2 = targetPlayerData.replaceAll("player_my_", "");
@@ -393,7 +612,8 @@ class PlacarController extends ChangeNotifier {
} }
} }
// Verifica se são da mesma equipa (Adversário) // Verifica se são da mesma equipa (Adversário)
else if (draggedPlayerData.startsWith("player_opp_") && targetPlayerData.startsWith("player_opp_")) { else if (draggedPlayerData.startsWith("player_opp_") &&
targetPlayerData.startsWith("player_opp_")) {
String id1 = draggedPlayerData.replaceAll("player_opp_", ""); String id1 = draggedPlayerData.replaceAll("player_opp_", "");
String id2 = targetPlayerData.replaceAll("player_opp_", ""); String id2 = targetPlayerData.replaceAll("player_opp_", "");
@@ -411,17 +631,38 @@ class PlacarController extends ChangeNotifier {
_scheduleAutoSave(); _scheduleAutoSave();
notifyListeners(); notifyListeners();
_dispatchSyncAction('swap_players', {
'dragged': draggedPlayerData,
'target': targetPlayerData,
});
} }
void registerShotFromPopup(BuildContext context, String action, String targetPlayer, String zone, int points, double relativeX, double relativeY) { void registerShotFromPopup(
String playerId = targetPlayer.replaceAll("player_my_", "").replaceAll("player_opp_", ""); BuildContext context,
String action,
String targetPlayer,
String zone,
int points,
double relativeX,
double relativeY,
) {
String playerId = targetPlayer
.replaceAll("player_my_", "")
.replaceAll("player_opp_", "");
bool isMake = action.startsWith("add_"); bool isMake = action.startsWith("add_");
String name = playerNames[playerId] ?? "Jogador"; String name = playerNames[playerId] ?? "Jogador";
matchShots.add(ShotRecord( matchShots.add(
relativeX: relativeX, relativeY: relativeY, isMake: isMake, ShotRecord(
playerId: playerId, playerName: name, zone: zone, points: points relativeX: relativeX,
)); relativeY: relativeY,
isMake: isMake,
playerId: playerId,
playerName: name,
zone: zone,
points: points,
),
);
String finalAction = isMake ? "add_pts_$points" : "miss_$points"; String finalAction = isMake ? "add_pts_$points" : "miss_$points";
commitStat(finalAction, targetPlayer); commitStat(finalAction, targetPlayer);
@@ -442,13 +683,25 @@ class PlacarController extends ChangeNotifier {
bool isMake = pendingAction!.startsWith("add_pts_"); bool isMake = pendingAction!.startsWith("add_pts_");
double relX = position.dx / size.width; double relX = position.dx / size.width;
double relY = position.dy / size.height; double relY = position.dy / size.height;
String pId = pendingPlayerId!.replaceAll("player_my_", "").replaceAll("player_opp_", ""); String pId = pendingPlayerId!
.replaceAll("player_my_", "")
.replaceAll("player_opp_", "");
matchShots.add(ShotRecord(relativeX: relX, relativeY: relY, isMake: isMake, playerId: pId, playerName: playerNames[pId]!)); matchShots.add(
ShotRecord(
relativeX: relX,
relativeY: relY,
isMake: isMake,
playerId: pId,
playerName: playerNames[pId]!,
),
);
commitStat(pendingAction!, pendingPlayerId!); commitStat(pendingAction!, pendingPlayerId!);
isSelectingShotLocation = false; pendingAction = null; pendingPlayerId = null; isSelectingShotLocation = false;
pendingAction = null;
pendingPlayerId = null;
_scheduleAutoSave(); _scheduleAutoSave();
notifyListeners(); notifyListeners();
} }
@@ -481,17 +734,25 @@ class PlacarController extends ChangeNotifier {
} }
void cancelShotLocation() { void cancelShotLocation() {
isSelectingShotLocation = false; pendingAction = null; pendingPlayerId = null; notifyListeners(); isSelectingShotLocation = false;
pendingAction = null;
pendingPlayerId = null;
notifyListeners();
} }
void registerFoul(String committerData, String foulType, String victimData) { void registerFoul(String committerData, String foulType, String victimData) {
bool isOpponent = committerData.startsWith("player_opp_"); bool isOpponent = committerData.startsWith("player_opp_");
String committerId = committerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); String committerId = committerData
.replaceAll("player_my_", "")
.replaceAll("player_opp_", "");
final committerStats = playerStats[committerId]!; final committerStats = playerStats[committerId]!;
final committerName = playerNames[committerId] ?? "Jogador"; final committerName = playerNames[committerId] ?? "Jogador";
committerStats["fls"] = committerStats["fls"]! + 1; committerStats["fls"] = committerStats["fls"]! + 1;
if (isOpponent) opponentFouls++; else myFouls++; if (isOpponent)
opponentFouls++;
else
myFouls++;
if (foulType == "Desqualificante") { if (foulType == "Desqualificante") {
committerStats["fls"] = 5; committerStats["fls"] = 5;
@@ -500,7 +761,9 @@ class PlacarController extends ChangeNotifier {
String logText = "cometeu Falta $foulType"; String logText = "cometeu Falta $foulType";
if (victimData.isNotEmpty) { if (victimData.isNotEmpty) {
String victimId = victimData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); String victimId = victimData
.replaceAll("player_my_", "")
.replaceAll("player_opp_", "");
final victimStats = playerStats[victimId]!; final victimStats = playerStats[victimId]!;
final victimName = playerNames[victimId] ?? "Jogador"; final victimName = playerNames[victimId] ?? "Jogador";
@@ -510,11 +773,17 @@ class PlacarController extends ChangeNotifier {
logText += " (Equipa/Banco) ⚠️"; logText += " (Equipa/Banco) ⚠️";
} }
String time = "${durationNotifier.value.inMinutes.toString().padLeft(2, '0')}:${durationNotifier.value.inSeconds.remainder(60).toString().padLeft(2, '0')}"; String time =
"${durationNotifier.value.inMinutes.toString().padLeft(2, '0')}:${durationNotifier.value.inSeconds.remainder(60).toString().padLeft(2, '0')}";
playByPlay.insert(0, "P$currentQuarter - $time: $committerName $logText"); playByPlay.insert(0, "P$currentQuarter - $time: $committerName $logText");
_scheduleAutoSave(); _scheduleAutoSave();
notifyListeners(); notifyListeners();
_dispatchSyncAction('register_foul', {
'committer': committerData,
'foulType': foulType,
'victim': victimData,
});
} }
void commitStat(String action, String playerData) { void commitStat(String action, String playerData) {
@@ -553,13 +822,14 @@ class PlacarController extends ChangeNotifier {
} }
logText = "marcou $pts pontos 🏀"; logText = "marcou $pts pontos 🏀";
} }
// ── ANULAR PONTOS ──────────────────────────────────────────────────────── // ── ANULAR PONTOS ────────────────────────────────────────────────────────
else if (action.startsWith("sub_pts_")) { else if (action.startsWith("sub_pts_")) {
int ptsToAnul = int.parse(action.split("_").last); int ptsToAnul = int.parse(action.split("_").last);
int lastShotIndex = matchShots.lastIndexWhere((s) => int lastShotIndex = matchShots.lastIndexWhere(
s.playerId == playerId && s.isMake == true && s.points == ptsToAnul); (s) =>
s.playerId == playerId && s.isMake == true && s.points == ptsToAnul,
);
if (lastShotIndex != -1) { if (lastShotIndex != -1) {
matchShots.removeAt(lastShotIndex); matchShots.removeAt(lastShotIndex);
@@ -588,7 +858,6 @@ class PlacarController extends ChangeNotifier {
return; return;
} }
} }
// ── FALHAS ─────────────────────────────────────────────────────────────── // ── FALHAS ───────────────────────────────────────────────────────────────
else if (action == "miss_1") { else if (action == "miss_1") {
stats["fta"] = stats["fta"]! + 1; stats["fta"] = stats["fta"]! + 1;
@@ -602,7 +871,6 @@ class PlacarController extends ChangeNotifier {
stats["p3a"] = stats["p3a"]! + 1; stats["p3a"] = stats["p3a"]! + 1;
logText = "falhou lançamento de 3 ❌"; logText = "falhou lançamento de 3 ❌";
} }
// ── RESSALTOS ───────────────────────────────────────────────────────────── // ── RESSALTOS ─────────────────────────────────────────────────────────────
else if (action == "add_orb") { else if (action == "add_orb") {
stats["orb"] = stats["orb"]! + 1; stats["orb"] = stats["orb"]! + 1;
@@ -613,19 +881,16 @@ class PlacarController extends ChangeNotifier {
stats["rbs"] = stats["rbs"]! + 1; stats["rbs"] = stats["rbs"]! + 1;
logText = "ganhou ressalto defensivo 🛡️"; logText = "ganhou ressalto defensivo 🛡️";
} }
// ── ASSISTÊNCIA ─────────────────────────────────────────────────────────── // ── ASSISTÊNCIA ───────────────────────────────────────────────────────────
else if (action == "add_ast") { else if (action == "add_ast") {
stats["ast"] = stats["ast"]! + 1; stats["ast"] = stats["ast"]! + 1;
logText = "fez uma assistência 🤝"; logText = "fez uma assistência 🤝";
} }
// ── SOFRIDAS ────────────────────────────────────────────────────────────── // ── SOFRIDAS ──────────────────────────────────────────────────────────────
else if (action == "add_so") { else if (action == "add_so") {
stats["so"] = stats["so"]! + 1; stats["so"] = stats["so"]! + 1;
logText = "sofreu uma falta 🤕"; logText = "sofreu uma falta 🤕";
} }
// ══════════════════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════════════════
// STEAL — ROUBO DE BOLA // STEAL — ROUBO DE BOLA
// ══════════════════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════════════════
@@ -637,7 +902,6 @@ class PlacarController extends ChangeNotifier {
stats["il"] = stats["il"]! + 1; stats["il"] = stats["il"]! + 1;
logText = "intercetou um lançamento 🛑"; logText = "intercetou um lançamento 🛑";
} }
// ══════════════════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════════════════
// BLOCK — DESARME // BLOCK — DESARME
// ══════════════════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════════════════
@@ -648,7 +912,6 @@ class PlacarController extends ChangeNotifier {
stats["li"] = stats["li"]! + 1; stats["li"] = stats["li"]! + 1;
logText = "sofreu um desarme 🚫"; logText = "sofreu um desarme 🚫";
} }
// Ações independentes legadas // Ações independentes legadas
else if (action == "add_il") { else if (action == "add_il") {
stats["il"] = stats["il"]! + 1; stats["il"] = stats["il"]! + 1;
@@ -657,7 +920,6 @@ class PlacarController extends ChangeNotifier {
stats["li"] = stats["li"]! + 1; stats["li"] = stats["li"]! + 1;
logText = "teve o lançamento intercetado 🚫"; logText = "teve o lançamento intercetado 🚫";
} }
// ══════════════════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════════════════
// TURNOVER — PERDE DE BOLA E INFRAÇÕES // TURNOVER — PERDE DE BOLA E INFRAÇÕES
// ══════════════════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════════════════
@@ -666,21 +928,20 @@ class PlacarController extends ChangeNotifier {
logText = "fez um passe ruim 🤦"; logText = "fez um passe ruim 🤦";
} else if (action == "tov_3s") { } else if (action == "tov_3s") {
stats["tres_seg"] = stats["tres_seg"]! + 1; // SOMA AOS 3 SEGUNDOS stats["tres_seg"] = stats["tres_seg"]! + 1; // SOMA AOS 3 SEGUNDOS
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
logText = "violação de 3 segundos ⏱️"; logText = "violação de 3 segundos ⏱️";
} else if (action == "tov_clock") { } else if (action == "tov_clock") {
stats["tov"] = stats["tov"]! + 1; stats["tov"] = stats["tov"]! + 1;
logText = "violação de 24 segundos ⏱️"; logText = "violação de 24 segundos ⏱️";
} else if (action == "tov_travel") { } else if (action == "tov_travel") {
stats["pa"] = stats["pa"]! + 1; // SOMA AOS PASSOS stats["pa"] = stats["pa"]! + 1; // SOMA AOS PASSOS
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
logText = "cometeu passos 🚶"; logText = "cometeu passos 🚶";
} else if (action == "tov_double") { } else if (action == "tov_double") {
stats["dr"] = stats["dr"]! + 1; // SOMA AOS DRIBLES DUPLOS stats["dr"] = stats["dr"]! + 1; // SOMA AOS DRIBLES DUPLOS
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
logText = "fez drible duplo 🏀"; logText = "fez drible duplo 🏀";
} }
// ── ANULAR FALTA ────────────────────────────────────────────────────────── // ── ANULAR FALTA ──────────────────────────────────────────────────────────
else if (action == "sub_foul") { else if (action == "sub_foul") {
if (stats["fls"]! > 0) stats["fls"] = stats["fls"]! - 1; if (stats["fls"]! > 0) stats["fls"] = stats["fls"]! - 1;
@@ -700,6 +961,10 @@ class PlacarController extends ChangeNotifier {
_scheduleAutoSave(); _scheduleAutoSave();
notifyListeners(); notifyListeners();
_dispatchSyncAction('commit_stat', {
'action': action,
'player_data': playerData,
});
} }
@override @override
@@ -744,7 +1009,7 @@ class PlacarController extends ChangeNotifier {
double mvpScore = double mvpScore =
((pts * 0.30) + (tr * 0.20) + (ast * 0.35) + (br * 0.15)) - ((pts * 0.30) + (tr * 0.20) + (ast * 0.35) + (br * 0.15)) -
((bp * 0.35) + (lFalhados * 0.30) + (llFalhados * 0.35)); ((bp * 0.35) + (lFalhados * 0.30) + (llFalhados * 0.35));
mvpScore = mvpScore * (minJogados / 40.0); mvpScore = mvpScore * (minJogados / 40.0);
String pName = playerNames[playerId] ?? '---'; String pName = playerNames[playerId] ?? '---';
@@ -768,20 +1033,23 @@ class PlacarController extends ChangeNotifier {
}); });
// 1. Atualizar o Jogo // 1. Atualizar o Jogo
await supabase.from('games').update({ await supabase
'my_score': myScore, .from('games')
'opponent_score': opponentScore, .update({
'remaining_seconds': durationNotifier.value.inSeconds, 'my_score': myScore,
'my_timeouts': myTimeoutsUsed, 'opponent_score': opponentScore,
'opp_timeouts': opponentTimeoutsUsed, 'remaining_seconds': durationNotifier.value.inSeconds,
'current_quarter': currentQuarter, 'my_timeouts': myTimeoutsUsed,
'status': newStatus, 'opp_timeouts': opponentTimeoutsUsed,
'top_pts_name': topPtsName, 'current_quarter': currentQuarter,
'top_ast_name': topAstName, 'status': newStatus,
'top_rbs_name': topRbsName, 'top_pts_name': topPtsName,
'mvp_name': mvpName, 'top_ast_name': topAstName,
'play_by_play': playByPlay, 'top_rbs_name': topRbsName,
}).eq('id', gameId); 'mvp_name': mvpName,
'play_by_play': playByPlay,
})
.eq('id', gameId);
// 2. Preparar as Estatísticas dos Jogadores // 2. Preparar as Estatísticas dos Jogadores
List<Map<String, dynamic>> batchStats = []; List<Map<String, dynamic>> batchStats = [];
@@ -855,16 +1123,22 @@ class PlacarController extends ChangeNotifier {
await prefs.remove('backup_$gameId'); await prefs.remove('backup_$gameId');
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Guardado com Sucesso!'), content: Text('Guardado com Sucesso!'),
backgroundColor: Colors.green)); backgroundColor: Colors.green,
),
);
} }
} catch (e) { } catch (e) {
debugPrint("Erro ao gravar estatísticas: $e"); debugPrint("Erro ao gravar estatísticas: $e");
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erro ao guardar: $e'), content: Text('Erro ao guardar: $e'),
backgroundColor: Colors.red)); backgroundColor: Colors.red,
),
);
} }
} finally { } finally {
isSaving = false; isSaving = false;

File diff suppressed because it is too large Load Diff

View File

@@ -71,6 +71,9 @@ flutter:
- assets/assit.png - assets/assit.png
- assets/tov.png - assets/tov.png
- assets/stl.png - assets/stl.png
- assets/campone.png
fonts: fonts:
- family: playmaker - family: playmaker
fonts: fonts:

View File

@@ -1,9 +1,3 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';