nao sei
This commit is contained in:
157
SYNC_CHANGES_SUMMARY.md
Normal file
157
SYNC_CHANGES_SUMMARY.md
Normal 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
BIN
assets/campone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 MiB |
@@ -1,3 +1,4 @@
|
||||
// ...existing code...
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'dart:math';
|
||||
|
||||
@@ -81,14 +82,18 @@ class GameSharingController {
|
||||
.from('profiles')
|
||||
.select('username, full_name')
|
||||
.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 {
|
||||
'session_id': session['id'],
|
||||
'game_id': gameId,
|
||||
'creator_name': creatorData['full_name'] ?? creatorData['username'] ?? 'Utilizador',
|
||||
'creator_name': creatorName,
|
||||
'game': gameData,
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -156,14 +161,16 @@ class GameSharingController {
|
||||
Future<bool> sendSyncEvent(
|
||||
String sessionId,
|
||||
String actionType,
|
||||
Map<String, dynamic> actionData,
|
||||
) async {
|
||||
Map<String, dynamic> actionData, {
|
||||
String? playerId, // opcional: identifica jogador/entidade alvo
|
||||
}) async {
|
||||
try {
|
||||
await _supabase.from('game_sync_events').insert({
|
||||
'session_id': sessionId,
|
||||
'action_type': actionType,
|
||||
'action_data': actionData,
|
||||
'triggered_by': myUserId,
|
||||
if (playerId != null) 'player_id': playerId,
|
||||
});
|
||||
|
||||
print("✅ Evento sincronizado: $actionType");
|
||||
@@ -186,6 +193,24 @@ class GameSharingController {
|
||||
.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
|
||||
// ====================================
|
||||
|
||||
@@ -25,13 +25,23 @@ class ShotRecord {
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'relativeX': relativeX, 'relativeY': relativeY, 'isMake': isMake,
|
||||
'playerId': playerId, 'playerName': playerName, 'zone': zone, 'points': points,
|
||||
'relativeX': relativeX,
|
||||
'relativeY': relativeY,
|
||||
'isMake': isMake,
|
||||
'playerId': playerId,
|
||||
'playerName': playerName,
|
||||
'zone': zone,
|
||||
'points': points,
|
||||
};
|
||||
|
||||
factory ShotRecord.fromJson(Map<String, dynamic> json) => ShotRecord(
|
||||
relativeX: json['relativeX'], relativeY: json['relativeY'], isMake: json['isMake'],
|
||||
playerId: json['playerId'], playerName: json['playerName'], zone: json['zone'], points: json['points'],
|
||||
relativeX: json['relativeX'],
|
||||
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 myTeam;
|
||||
final String opponentTeam;
|
||||
final void Function(String actionType, Map<String, dynamic> actionData)?
|
||||
onSyncAction;
|
||||
|
||||
PlacarController({
|
||||
required this.gameId,
|
||||
required this.myTeam,
|
||||
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 isSaving = false;
|
||||
bool gameWasAlreadyFinished = false;
|
||||
@@ -80,7 +173,9 @@ class PlacarController extends ChangeNotifier {
|
||||
|
||||
List<String> playByPlay = [];
|
||||
|
||||
ValueNotifier<Duration> durationNotifier = ValueNotifier(const Duration(minutes: 10));
|
||||
ValueNotifier<Duration> durationNotifier = ValueNotifier(
|
||||
const Duration(minutes: 10),
|
||||
);
|
||||
Timer? timer;
|
||||
bool isRunning = false;
|
||||
|
||||
@@ -96,21 +191,41 @@ class PlacarController extends ChangeNotifier {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 1500));
|
||||
|
||||
myCourt.clear(); myBench.clear(); oppCourt.clear(); oppBench.clear();
|
||||
playerNames.clear(); playerStats.clear(); playerNumbers.clear();
|
||||
matchShots.clear(); playByPlay.clear(); myFouls = 0; opponentFouls = 0;
|
||||
myCourt.clear();
|
||||
myBench.clear();
|
||||
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;
|
||||
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);
|
||||
|
||||
myTimeoutsUsed = int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0;
|
||||
opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
|
||||
currentQuarter = int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1;
|
||||
myTimeoutsUsed =
|
||||
int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0;
|
||||
opponentTimeoutsUsed =
|
||||
int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
|
||||
currentQuarter =
|
||||
int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1;
|
||||
|
||||
gameWasAlreadyFinished = gameResponse['status'] == 'Terminado';
|
||||
|
||||
@@ -120,25 +235,49 @@ class PlacarController extends ChangeNotifier {
|
||||
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) {
|
||||
if (t['name'] == myTeam) myTeamDbId = 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> oppPlayers = oppTeamDbId != null ? await supabase.from('members').select().eq('team_id', oppTeamDbId!).eq('type', 'Jogador') : [];
|
||||
List<dynamic> myPlayers = myTeamDbId != null
|
||||
? 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 = {
|
||||
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++) {
|
||||
String dbId = myPlayers[i]['id'].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)) {
|
||||
var s = savedStats[dbId];
|
||||
@@ -152,7 +291,13 @@ class PlacarController extends ChangeNotifier {
|
||||
String dbId = oppPlayers[i]['id'].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)) {
|
||||
var s = savedStats[dbId];
|
||||
@@ -162,17 +307,24 @@ class PlacarController extends ChangeNotifier {
|
||||
}
|
||||
_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) {
|
||||
matchShots.add(ShotRecord(
|
||||
matchShots.add(
|
||||
ShotRecord(
|
||||
relativeX: double.parse(shotData['relative_x'].toString()),
|
||||
relativeY: double.parse(shotData['relative_y'].toString()),
|
||||
isMake: shotData['is_make'] == true,
|
||||
playerId: shotData['member_id'].toString(),
|
||||
playerName: shotData['player_name'].toString(),
|
||||
zone: shotData['zone']?.toString(),
|
||||
points: shotData['points'] != null ? int.parse(shotData['points'].toString()) : null,
|
||||
));
|
||||
points: shotData['points'] != null
|
||||
? int.parse(shotData['points'].toString())
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await _loadLocalBackup();
|
||||
@@ -188,42 +340,103 @@ class PlacarController extends ChangeNotifier {
|
||||
|
||||
void _loadSavedPlayerStats(String dbId, Map<String, dynamic> s) {
|
||||
playerStats[dbId] = {
|
||||
"pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0,
|
||||
"stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 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,
|
||||
"pts": s['pts'] ?? 0,
|
||||
"rbs": s['rbs'] ?? 0,
|
||||
"ast": s['ast'] ?? 0,
|
||||
"stl": s['stl'] ?? 0,
|
||||
"tov": s['tov'] ?? 0,
|
||||
"blk": s['blk'] ?? 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,
|
||||
"sec": s['minutos_jogados'] ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
void _registerPlayer({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)}";
|
||||
void _registerPlayer({
|
||||
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;
|
||||
playerNumbers[id] = number;
|
||||
|
||||
playerStats[id] = {
|
||||
"pts": 0, "rbs": 0, "ast": 0, "stl": 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
|
||||
"pts": 0,
|
||||
"rbs": 0,
|
||||
"ast": 0,
|
||||
"stl": 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 (isCourt) myCourt.add(id); else myBench.add(id);
|
||||
if (isCourt)
|
||||
myCourt.add(id);
|
||||
else
|
||||
myBench.add(id);
|
||||
} 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) {
|
||||
_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 {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final backupData = {
|
||||
'myScore': myScore, 'opponentScore': opponentScore,
|
||||
'myFouls': myFouls, 'opponentFouls': opponentFouls,
|
||||
'currentQuarter': currentQuarter, 'duration': durationNotifier.value.inSeconds,
|
||||
'myTimeoutsUsed': myTimeoutsUsed, 'opponentTimeoutsUsed': opponentTimeoutsUsed,
|
||||
'myScore': myScore,
|
||||
'opponentScore': opponentScore,
|
||||
'myFouls': myFouls,
|
||||
'opponentFouls': opponentFouls,
|
||||
'currentQuarter': currentQuarter,
|
||||
'duration': durationNotifier.value.inSeconds,
|
||||
'myTimeoutsUsed': myTimeoutsUsed,
|
||||
'opponentTimeoutsUsed': opponentTimeoutsUsed,
|
||||
'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(),
|
||||
'playByPlay': playByPlay,
|
||||
};
|
||||
@@ -261,16 +481,24 @@ class PlacarController extends ChangeNotifier {
|
||||
if (backupString != null) {
|
||||
final data = jsonDecode(backupString);
|
||||
|
||||
myScore = data['myScore']; opponentScore = data['opponentScore'];
|
||||
myFouls = data['myFouls']; opponentFouls = data['opponentFouls'];
|
||||
currentQuarter = data['currentQuarter']; durationNotifier.value = Duration(seconds: data['duration']);
|
||||
myTimeoutsUsed = data['myTimeoutsUsed']; opponentTimeoutsUsed = data['opponentTimeoutsUsed'];
|
||||
myScore = data['myScore'];
|
||||
opponentScore = data['opponentScore'];
|
||||
myFouls = data['myFouls'];
|
||||
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']);
|
||||
oppCourt = List<String>.from(data['oppCourt']); oppBench = List<String>.from(data['oppBench']);
|
||||
myCourt = List<String>.from(data['myCourt']);
|
||||
myBench = List<String>.from(data['myBench']);
|
||||
oppCourt = List<String>.from(data['oppCourt']);
|
||||
oppBench = List<String>.from(data['oppBench']);
|
||||
|
||||
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'];
|
||||
matchShots = decodedShots.map((s) => ShotRecord.fromJson(s)).toList();
|
||||
@@ -283,43 +511,8 @@ class PlacarController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void toggleTimer(BuildContext context) {
|
||||
if (isRunning) {
|
||||
timer?.cancel();
|
||||
_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();
|
||||
print("⏱️ toggleTimer chamado: isRunning=$isRunning");
|
||||
_setTimerRunning(!isRunning);
|
||||
}
|
||||
|
||||
void useTimeout(bool isOpponent) {
|
||||
@@ -332,19 +525,34 @@ class PlacarController extends ChangeNotifier {
|
||||
timer?.cancel();
|
||||
_scheduleAutoSave();
|
||||
notifyListeners();
|
||||
_dispatchSyncAction('use_timeout', {'is_opponent': isOpponent});
|
||||
}
|
||||
|
||||
void handleActionDrag(BuildContext context, String action, String playerData) {
|
||||
String playerId = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
||||
void handleActionDrag(
|
||||
BuildContext context,
|
||||
String action,
|
||||
String playerData,
|
||||
) {
|
||||
String playerId = playerData
|
||||
.replaceAll("player_my_", "")
|
||||
.replaceAll("player_opp_", "");
|
||||
final stats = playerStats[playerId]!;
|
||||
final name = playerNames[playerId]!;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
pendingPlayerId = playerData;
|
||||
isSelectingShotLocation = true;
|
||||
@@ -354,7 +562,12 @@ class PlacarController extends ChangeNotifier {
|
||||
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) {
|
||||
String benchPlayerId = action.replaceAll("bench_my_", "");
|
||||
if (playerStats[benchPlayerId]!["fls"]! >= 5) return;
|
||||
@@ -375,12 +588,18 @@ class PlacarController extends ChangeNotifier {
|
||||
}
|
||||
_scheduleAutoSave();
|
||||
notifyListeners();
|
||||
_dispatchSyncAction('subbing', {
|
||||
'action': action,
|
||||
'court_player': courtPlayerId,
|
||||
'is_opponent': isOpponent,
|
||||
});
|
||||
}
|
||||
|
||||
// ── TROCAR JOGADORES NO CAMPO ──────────────────────────────────────────────
|
||||
void swapCourtPlayers(String draggedPlayerData, String targetPlayerData) {
|
||||
// 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 id2 = targetPlayerData.replaceAll("player_my_", "");
|
||||
|
||||
@@ -393,7 +612,8 @@ class PlacarController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
// 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 id2 = targetPlayerData.replaceAll("player_opp_", "");
|
||||
|
||||
@@ -411,17 +631,38 @@ class PlacarController extends ChangeNotifier {
|
||||
|
||||
_scheduleAutoSave();
|
||||
notifyListeners();
|
||||
_dispatchSyncAction('swap_players', {
|
||||
'dragged': draggedPlayerData,
|
||||
'target': targetPlayerData,
|
||||
});
|
||||
}
|
||||
|
||||
void registerShotFromPopup(BuildContext context, String action, String targetPlayer, String zone, int points, double relativeX, double relativeY) {
|
||||
String playerId = targetPlayer.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
||||
void registerShotFromPopup(
|
||||
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_");
|
||||
String name = playerNames[playerId] ?? "Jogador";
|
||||
|
||||
matchShots.add(ShotRecord(
|
||||
relativeX: relativeX, relativeY: relativeY, isMake: isMake,
|
||||
playerId: playerId, playerName: name, zone: zone, points: points
|
||||
));
|
||||
matchShots.add(
|
||||
ShotRecord(
|
||||
relativeX: relativeX,
|
||||
relativeY: relativeY,
|
||||
isMake: isMake,
|
||||
playerId: playerId,
|
||||
playerName: name,
|
||||
zone: zone,
|
||||
points: points,
|
||||
),
|
||||
);
|
||||
|
||||
String finalAction = isMake ? "add_pts_$points" : "miss_$points";
|
||||
commitStat(finalAction, targetPlayer);
|
||||
@@ -442,13 +683,25 @@ class PlacarController extends ChangeNotifier {
|
||||
bool isMake = pendingAction!.startsWith("add_pts_");
|
||||
double relX = position.dx / size.width;
|
||||
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!);
|
||||
|
||||
isSelectingShotLocation = false; pendingAction = null; pendingPlayerId = null;
|
||||
isSelectingShotLocation = false;
|
||||
pendingAction = null;
|
||||
pendingPlayerId = null;
|
||||
_scheduleAutoSave();
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -481,17 +734,25 @@ class PlacarController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void cancelShotLocation() {
|
||||
isSelectingShotLocation = false; pendingAction = null; pendingPlayerId = null; notifyListeners();
|
||||
isSelectingShotLocation = false;
|
||||
pendingAction = null;
|
||||
pendingPlayerId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void registerFoul(String committerData, String foulType, String victimData) {
|
||||
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 committerName = playerNames[committerId] ?? "Jogador";
|
||||
|
||||
committerStats["fls"] = committerStats["fls"]! + 1;
|
||||
if (isOpponent) opponentFouls++; else myFouls++;
|
||||
if (isOpponent)
|
||||
opponentFouls++;
|
||||
else
|
||||
myFouls++;
|
||||
|
||||
if (foulType == "Desqualificante") {
|
||||
committerStats["fls"] = 5;
|
||||
@@ -500,7 +761,9 @@ class PlacarController extends ChangeNotifier {
|
||||
String logText = "cometeu Falta $foulType";
|
||||
|
||||
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 victimName = playerNames[victimId] ?? "Jogador";
|
||||
|
||||
@@ -510,11 +773,17 @@ class PlacarController extends ChangeNotifier {
|
||||
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");
|
||||
|
||||
_scheduleAutoSave();
|
||||
notifyListeners();
|
||||
_dispatchSyncAction('register_foul', {
|
||||
'committer': committerData,
|
||||
'foulType': foulType,
|
||||
'victim': victimData,
|
||||
});
|
||||
}
|
||||
|
||||
void commitStat(String action, String playerData) {
|
||||
@@ -553,13 +822,14 @@ class PlacarController extends ChangeNotifier {
|
||||
}
|
||||
logText = "marcou $pts pontos 🏀";
|
||||
}
|
||||
|
||||
// ── ANULAR PONTOS ────────────────────────────────────────────────────────
|
||||
else if (action.startsWith("sub_pts_")) {
|
||||
int ptsToAnul = int.parse(action.split("_").last);
|
||||
|
||||
int lastShotIndex = matchShots.lastIndexWhere((s) =>
|
||||
s.playerId == playerId && s.isMake == true && s.points == ptsToAnul);
|
||||
int lastShotIndex = matchShots.lastIndexWhere(
|
||||
(s) =>
|
||||
s.playerId == playerId && s.isMake == true && s.points == ptsToAnul,
|
||||
);
|
||||
|
||||
if (lastShotIndex != -1) {
|
||||
matchShots.removeAt(lastShotIndex);
|
||||
@@ -588,7 +858,6 @@ class PlacarController extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ── FALHAS ───────────────────────────────────────────────────────────────
|
||||
else if (action == "miss_1") {
|
||||
stats["fta"] = stats["fta"]! + 1;
|
||||
@@ -602,7 +871,6 @@ class PlacarController extends ChangeNotifier {
|
||||
stats["p3a"] = stats["p3a"]! + 1;
|
||||
logText = "falhou lançamento de 3 ❌";
|
||||
}
|
||||
|
||||
// ── RESSALTOS ─────────────────────────────────────────────────────────────
|
||||
else if (action == "add_orb") {
|
||||
stats["orb"] = stats["orb"]! + 1;
|
||||
@@ -613,19 +881,16 @@ class PlacarController extends ChangeNotifier {
|
||||
stats["rbs"] = stats["rbs"]! + 1;
|
||||
logText = "ganhou ressalto defensivo 🛡️";
|
||||
}
|
||||
|
||||
// ── ASSISTÊNCIA ───────────────────────────────────────────────────────────
|
||||
else if (action == "add_ast") {
|
||||
stats["ast"] = stats["ast"]! + 1;
|
||||
logText = "fez uma assistência 🤝";
|
||||
}
|
||||
|
||||
// ── SOFRIDAS ──────────────────────────────────────────────────────────────
|
||||
else if (action == "add_so") {
|
||||
stats["so"] = stats["so"]! + 1;
|
||||
logText = "sofreu uma falta 🤕";
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// STEAL — ROUBO DE BOLA
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
@@ -637,7 +902,6 @@ class PlacarController extends ChangeNotifier {
|
||||
stats["il"] = stats["il"]! + 1;
|
||||
logText = "intercetou um lançamento 🛑";
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// BLOCK — DESARME
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
@@ -648,7 +912,6 @@ class PlacarController extends ChangeNotifier {
|
||||
stats["li"] = stats["li"]! + 1;
|
||||
logText = "sofreu um desarme 🚫";
|
||||
}
|
||||
|
||||
// Ações independentes legadas
|
||||
else if (action == "add_il") {
|
||||
stats["il"] = stats["il"]! + 1;
|
||||
@@ -657,7 +920,6 @@ class PlacarController extends ChangeNotifier {
|
||||
stats["li"] = stats["li"]! + 1;
|
||||
logText = "teve o lançamento intercetado 🚫";
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// TURNOVER — PERDE DE BOLA E INFRAÇÕES
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
@@ -680,7 +942,6 @@ class PlacarController extends ChangeNotifier {
|
||||
stats["tov"] = stats["tov"]! + 1; // SOMA AO TURNOVER GERAL
|
||||
logText = "fez drible duplo 🏀";
|
||||
}
|
||||
|
||||
// ── ANULAR FALTA ──────────────────────────────────────────────────────────
|
||||
else if (action == "sub_foul") {
|
||||
if (stats["fls"]! > 0) stats["fls"] = stats["fls"]! - 1;
|
||||
@@ -700,6 +961,10 @@ class PlacarController extends ChangeNotifier {
|
||||
|
||||
_scheduleAutoSave();
|
||||
notifyListeners();
|
||||
_dispatchSyncAction('commit_stat', {
|
||||
'action': action,
|
||||
'player_data': playerData,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -768,7 +1033,9 @@ class PlacarController extends ChangeNotifier {
|
||||
});
|
||||
|
||||
// 1. Atualizar o Jogo
|
||||
await supabase.from('games').update({
|
||||
await supabase
|
||||
.from('games')
|
||||
.update({
|
||||
'my_score': myScore,
|
||||
'opponent_score': opponentScore,
|
||||
'remaining_seconds': durationNotifier.value.inSeconds,
|
||||
@@ -781,7 +1048,8 @@ class PlacarController extends ChangeNotifier {
|
||||
'top_rbs_name': topRbsName,
|
||||
'mvp_name': mvpName,
|
||||
'play_by_play': playByPlay,
|
||||
}).eq('id', gameId);
|
||||
})
|
||||
.eq('id', gameId);
|
||||
|
||||
// 2. Preparar as Estatísticas dos Jogadores
|
||||
List<Map<String, dynamic>> batchStats = [];
|
||||
@@ -855,16 +1123,22 @@ class PlacarController extends ChangeNotifier {
|
||||
await prefs.remove('backup_$gameId');
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Guardado com Sucesso!'),
|
||||
backgroundColor: Colors.green));
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Erro ao gravar estatísticas: $e");
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erro ao guardar: $e'),
|
||||
backgroundColor: Colors.red));
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
isSaving = false;
|
||||
|
||||
@@ -34,6 +34,7 @@ class _PlacarPageState extends State<PlacarPage> {
|
||||
String _sharedWithName = '';
|
||||
StreamSubscription? _syncSubscription;
|
||||
bool _isApplyingRemoteSync = false;
|
||||
final Set<String> _appliedSyncEventIds = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -47,6 +48,7 @@ class _PlacarPageState extends State<PlacarPage> {
|
||||
gameId: widget.gameId,
|
||||
myTeam: widget.myTeam,
|
||||
opponentTeam: widget.opponentTeam,
|
||||
onSyncAction: _onLocalControllerSync,
|
||||
);
|
||||
_controller.loadPlayers().then((_) => _initializeShareForGame());
|
||||
}
|
||||
@@ -190,58 +192,181 @@ class _PlacarPageState extends State<PlacarPage> {
|
||||
void _setupSyncListener() {
|
||||
if (_sessionId == null) return;
|
||||
_syncSubscription?.cancel();
|
||||
_syncSubscription = _sharingController.listenToGameSync(_sessionId!).listen(
|
||||
(event) {
|
||||
_appliedSyncEventIds.clear();
|
||||
_syncSubscription = _sharingController
|
||||
.listenToGameSyncOthers(_sessionId!)
|
||||
.listen(
|
||||
(dynamic event) {
|
||||
final rows = <Map<String, dynamic>>[];
|
||||
|
||||
if (event is List && event.isNotEmpty) {
|
||||
final record = event.last as Map<String, dynamic>?;
|
||||
if (record != null) {
|
||||
_handleSyncRecords(record);
|
||||
for (final item in event) {
|
||||
final row = item as Map<String, dynamic>?;
|
||||
if (row == null) continue;
|
||||
|
||||
final rowId = row['id']?.toString();
|
||||
if (rowId == null || _appliedSyncEventIds.contains(rowId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.add(Map<String, dynamic>.from(row));
|
||||
}
|
||||
} else if (event is Map<String, dynamic>) {
|
||||
final row = Map<String, dynamic>.from(event);
|
||||
final rowId = row['id']?.toString();
|
||||
if (rowId != null && !_appliedSyncEventIds.contains(rowId)) {
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.isEmpty) return;
|
||||
|
||||
rows.sort((a, b) {
|
||||
final aTime = a['created_at']?.toString();
|
||||
final bTime = b['created_at']?.toString();
|
||||
if (aTime == null || bTime == null) return 0;
|
||||
try {
|
||||
return DateTime.parse(aTime).compareTo(DateTime.parse(bTime));
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
for (final record in rows) {
|
||||
final recordId = record['id']?.toString();
|
||||
if (recordId == null || _appliedSyncEventIds.contains(recordId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_appliedSyncEventIds.add(recordId);
|
||||
print(
|
||||
"🔄 Evento remoto recebido: ${record['action_type']} - ${record['action_data']}",
|
||||
);
|
||||
_applyRemoteSyncEvent(record);
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
print("⚠️ Erro no stream de sync: $error");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSyncRecords(Map<String, dynamic> record) {
|
||||
final triggeredBy = record['triggered_by']?.toString();
|
||||
final currentUserId = Supabase.instance.client.auth.currentUser?.id;
|
||||
if (triggeredBy == null || triggeredBy == currentUserId) return;
|
||||
|
||||
// Mantido apenas como fallback, mas a escuta principal usa listenToGameSyncOthers.
|
||||
_applyRemoteSyncEvent(record);
|
||||
}
|
||||
|
||||
void _onLocalControllerSync(
|
||||
String actionType,
|
||||
Map<String, dynamic> actionData,
|
||||
) {
|
||||
if (_sessionId == null || _isApplyingRemoteSync) return;
|
||||
print("📤 Enviando sync action local: $actionType -> $actionData (is_running: ${actionData['is_running']})");
|
||||
_sharingController.sendSyncEvent(_sessionId!, actionType, actionData);
|
||||
}
|
||||
|
||||
void _applyRemoteSyncEvent(Map<String, dynamic> record) {
|
||||
final actionType = record['action_type']?.toString();
|
||||
final actionData = Map<String, dynamic>.from(record['action_data'] ?? {});
|
||||
if (actionType == 'toggle_timer') {
|
||||
final paused = actionData['paused'] == true;
|
||||
final remainingSeconds =
|
||||
int.tryParse(actionData['remaining_seconds']?.toString() ?? '') ??
|
||||
_controller.durationNotifier.value.inSeconds;
|
||||
_controller.durationNotifier.value = Duration(seconds: remainingSeconds);
|
||||
|
||||
if (paused && _controller.isRunning) {
|
||||
// Aplicar estado remoto do timer 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);
|
||||
}
|
||||
|
||||
final remoteQuarter = int.tryParse(
|
||||
actionData['current_quarter']?.toString() ?? '',
|
||||
);
|
||||
if (remoteQuarter != null) {
|
||||
_controller.currentQuarter = remoteQuarter;
|
||||
}
|
||||
|
||||
final remoteMyFouls = int.tryParse(
|
||||
actionData['my_fouls']?.toString() ?? '',
|
||||
);
|
||||
if (remoteMyFouls != null) {
|
||||
_controller.myFouls = remoteMyFouls;
|
||||
}
|
||||
|
||||
final remoteOpponentFouls = int.tryParse(
|
||||
actionData['opponent_fouls']?.toString() ?? '',
|
||||
);
|
||||
if (remoteOpponentFouls != null) {
|
||||
_controller.opponentFouls = remoteOpponentFouls;
|
||||
}
|
||||
|
||||
final remoteMyTimeoutsUsed = int.tryParse(
|
||||
actionData['my_timeouts_used']?.toString() ?? '',
|
||||
);
|
||||
if (remoteMyTimeoutsUsed != null) {
|
||||
_controller.myTimeoutsUsed = remoteMyTimeoutsUsed;
|
||||
}
|
||||
|
||||
final remoteOpponentTimeoutsUsed = int.tryParse(
|
||||
actionData['opponent_timeouts_used']?.toString() ?? '',
|
||||
);
|
||||
if (remoteOpponentTimeoutsUsed != null) {
|
||||
_controller.opponentTimeoutsUsed = remoteOpponentTimeoutsUsed;
|
||||
}
|
||||
|
||||
if (remoteIsRunning != _controller.isRunning) {
|
||||
_isApplyingRemoteSync = true;
|
||||
_controller.toggleTimer(context);
|
||||
_isApplyingRemoteSync = false;
|
||||
} else if (!paused && !_controller.isRunning) {
|
||||
_isApplyingRemoteSync = true;
|
||||
_controller.toggleTimer(context);
|
||||
_controller.applyRemoteTimerState(remoteIsRunning);
|
||||
_controller.notifyListeners();
|
||||
_isApplyingRemoteSync = false;
|
||||
}
|
||||
|
||||
if (actionType == 'toggle_timer') {
|
||||
setState(() {});
|
||||
} else if (actionType == 'commit_stat') {
|
||||
final action = actionData['action']?.toString() ?? '';
|
||||
final playerData = actionData['player_data']?.toString() ?? '';
|
||||
if (action.isNotEmpty && playerData.isNotEmpty) {
|
||||
_isApplyingRemoteSync = true;
|
||||
_controller.commitStat(action, playerData);
|
||||
_isApplyingRemoteSync = false;
|
||||
}
|
||||
} else if (actionType == 'register_foul') {
|
||||
final committer = actionData['committer']?.toString() ?? '';
|
||||
final foulType = actionData['foulType']?.toString() ?? '';
|
||||
final victim = actionData['victim']?.toString() ?? '';
|
||||
if (committer.isNotEmpty && foulType.isNotEmpty) {
|
||||
_isApplyingRemoteSync = true;
|
||||
_controller.registerFoul(committer, foulType, victim);
|
||||
_isApplyingRemoteSync = false;
|
||||
}
|
||||
} else if (actionType == 'subbing') {
|
||||
final action = actionData['action']?.toString() ?? '';
|
||||
final courtPlayer = actionData['court_player']?.toString() ?? '';
|
||||
final isOpponent = actionData['is_opponent'] == true;
|
||||
if (action.isNotEmpty && courtPlayer.isNotEmpty) {
|
||||
_isApplyingRemoteSync = true;
|
||||
_controller.handleSubbing(context, action, courtPlayer, isOpponent);
|
||||
_isApplyingRemoteSync = false;
|
||||
}
|
||||
} else if (actionType == 'swap_players') {
|
||||
final dragged = actionData['dragged']?.toString() ?? '';
|
||||
final target = actionData['target']?.toString() ?? '';
|
||||
if (dragged.isNotEmpty && target.isNotEmpty) {
|
||||
_isApplyingRemoteSync = true;
|
||||
_controller.swapCourtPlayers(dragged, target);
|
||||
_isApplyingRemoteSync = false;
|
||||
}
|
||||
} else if (actionType == 'use_timeout') {
|
||||
final isOpponent = actionData['is_opponent'] == true;
|
||||
_isApplyingRemoteSync = true;
|
||||
_controller.useTimeout(isOpponent);
|
||||
_isApplyingRemoteSync = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTimerButton(BuildContext context) {
|
||||
_controller.toggleTimer(context);
|
||||
|
||||
if (_sessionId != null && !_isApplyingRemoteSync) {
|
||||
_sharingController.sendSyncEvent(_sessionId!, 'toggle_timer', {
|
||||
'paused': !_controller.isRunning,
|
||||
'remaining_seconds': _controller.durationNotifier.value.inSeconds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openShareDialog(BuildContext context) async {
|
||||
@@ -397,7 +522,7 @@ class _PlacarPageState extends State<PlacarPage> {
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/campo.png'),
|
||||
image: AssetImage('assets/campone.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -71,6 +71,9 @@ flutter:
|
||||
- assets/assit.png
|
||||
- assets/tov.png
|
||||
- assets/stl.png
|
||||
- assets/campone.png
|
||||
|
||||
|
||||
fonts:
|
||||
- family: playmaker
|
||||
fonts:
|
||||
|
||||
@@ -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_test/flutter_test.dart';
|
||||
|
||||
Reference in New Issue
Block a user