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 '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
|
||||||
// ====================================
|
// ====================================
|
||||||
|
|||||||
@@ -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(
|
||||||
|
ShotRecord(
|
||||||
relativeX: double.parse(shotData['relative_x'].toString()),
|
relativeX: double.parse(shotData['relative_x'].toString()),
|
||||||
relativeY: double.parse(shotData['relative_y'].toString()),
|
relativeY: double.parse(shotData['relative_y'].toString()),
|
||||||
isMake: shotData['is_make'] == true,
|
isMake: shotData['is_make'] == true,
|
||||||
playerId: shotData['member_id'].toString(),
|
playerId: shotData['member_id'].toString(),
|
||||||
playerName: shotData['player_name'].toString(),
|
playerName: shotData['player_name'].toString(),
|
||||||
zone: shotData['zone']?.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();
|
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
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -680,7 +942,6 @@ class PlacarController extends ChangeNotifier {
|
|||||||
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
|
||||||
@@ -768,7 +1033,9 @@ class PlacarController extends ChangeNotifier {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 1. Atualizar o Jogo
|
// 1. Atualizar o Jogo
|
||||||
await supabase.from('games').update({
|
await supabase
|
||||||
|
.from('games')
|
||||||
|
.update({
|
||||||
'my_score': myScore,
|
'my_score': myScore,
|
||||||
'opponent_score': opponentScore,
|
'opponent_score': opponentScore,
|
||||||
'remaining_seconds': durationNotifier.value.inSeconds,
|
'remaining_seconds': durationNotifier.value.inSeconds,
|
||||||
@@ -781,7 +1048,8 @@ class PlacarController extends ChangeNotifier {
|
|||||||
'top_rbs_name': topRbsName,
|
'top_rbs_name': topRbsName,
|
||||||
'mvp_name': mvpName,
|
'mvp_name': mvpName,
|
||||||
'play_by_play': playByPlay,
|
'play_by_play': playByPlay,
|
||||||
}).eq('id', gameId);
|
})
|
||||||
|
.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;
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:playmaker/icons.dart/resaltosicon.dart';
|
import 'package:playmaker/icons.dart/resaltosicon.dart';
|
||||||
import 'package:playmaker/widgets/placar_widgets.dart'; // Mantém este import
|
import 'package:playmaker/widgets/placar_widgets.dart'; // Mantém este import
|
||||||
import 'package:playmaker/widgets/share_game_dialog.dart';
|
import 'package:playmaker/widgets/share_game_dialog.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
import '../classe/theme.dart';
|
import '../classe/theme.dart';
|
||||||
import '../controllers/game_sharing_controller.dart';
|
import '../controllers/game_sharing_controller.dart';
|
||||||
import '../controllers/placar_controller.dart';
|
import '../controllers/placar_controller.dart';
|
||||||
|
|
||||||
class PlacarPage extends StatefulWidget {
|
class PlacarPage extends StatefulWidget {
|
||||||
final String gameId, myTeam, opponentTeam;
|
final String gameId, myTeam, opponentTeam;
|
||||||
|
|
||||||
const PlacarPage({
|
const PlacarPage({
|
||||||
@@ -24,9 +24,9 @@ class PlacarPage extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlacarPage> createState() => _PlacarPageState();
|
State<PlacarPage> createState() => _PlacarPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PlacarPageState extends State<PlacarPage> {
|
class _PlacarPageState extends State<PlacarPage> {
|
||||||
late PlacarController _controller;
|
late PlacarController _controller;
|
||||||
final GameSharingController _sharingController = GameSharingController();
|
final GameSharingController _sharingController = GameSharingController();
|
||||||
String? _sessionId;
|
String? _sessionId;
|
||||||
@@ -34,6 +34,7 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
String _sharedWithName = '';
|
String _sharedWithName = '';
|
||||||
StreamSubscription? _syncSubscription;
|
StreamSubscription? _syncSubscription;
|
||||||
bool _isApplyingRemoteSync = false;
|
bool _isApplyingRemoteSync = false;
|
||||||
|
final Set<String> _appliedSyncEventIds = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -47,6 +48,7 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
gameId: widget.gameId,
|
gameId: widget.gameId,
|
||||||
myTeam: widget.myTeam,
|
myTeam: widget.myTeam,
|
||||||
opponentTeam: widget.opponentTeam,
|
opponentTeam: widget.opponentTeam,
|
||||||
|
onSyncAction: _onLocalControllerSync,
|
||||||
);
|
);
|
||||||
_controller.loadPlayers().then((_) => _initializeShareForGame());
|
_controller.loadPlayers().then((_) => _initializeShareForGame());
|
||||||
}
|
}
|
||||||
@@ -190,58 +192,181 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
void _setupSyncListener() {
|
void _setupSyncListener() {
|
||||||
if (_sessionId == null) return;
|
if (_sessionId == null) return;
|
||||||
_syncSubscription?.cancel();
|
_syncSubscription?.cancel();
|
||||||
_syncSubscription = _sharingController.listenToGameSync(_sessionId!).listen(
|
_appliedSyncEventIds.clear();
|
||||||
(event) {
|
_syncSubscription = _sharingController
|
||||||
|
.listenToGameSyncOthers(_sessionId!)
|
||||||
|
.listen(
|
||||||
|
(dynamic event) {
|
||||||
|
final rows = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
if (event is List && event.isNotEmpty) {
|
if (event is List && event.isNotEmpty) {
|
||||||
final record = event.last as Map<String, dynamic>?;
|
for (final item in event) {
|
||||||
if (record != null) {
|
final row = item as Map<String, dynamic>?;
|
||||||
_handleSyncRecords(record);
|
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) {
|
void _handleSyncRecords(Map<String, dynamic> record) {
|
||||||
final triggeredBy = record['triggered_by']?.toString();
|
// Mantido apenas como fallback, mas a escuta principal usa listenToGameSyncOthers.
|
||||||
final currentUserId = Supabase.instance.client.auth.currentUser?.id;
|
|
||||||
if (triggeredBy == null || triggeredBy == currentUserId) return;
|
|
||||||
|
|
||||||
_applyRemoteSyncEvent(record);
|
_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) {
|
void _applyRemoteSyncEvent(Map<String, dynamic> record) {
|
||||||
final actionType = record['action_type']?.toString();
|
final actionType = record['action_type']?.toString();
|
||||||
final actionData = Map<String, dynamic>.from(record['action_data'] ?? {});
|
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;
|
_isApplyingRemoteSync = true;
|
||||||
_controller.toggleTimer(context);
|
_controller.applyRemoteTimerState(remoteIsRunning);
|
||||||
_isApplyingRemoteSync = false;
|
_controller.notifyListeners();
|
||||||
} else if (!paused && !_controller.isRunning) {
|
|
||||||
_isApplyingRemoteSync = true;
|
|
||||||
_controller.toggleTimer(context);
|
|
||||||
_isApplyingRemoteSync = false;
|
_isApplyingRemoteSync = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actionType == 'toggle_timer') {
|
||||||
setState(() {});
|
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) {
|
void _handleTimerButton(BuildContext context) {
|
||||||
_controller.toggleTimer(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 {
|
Future<void> _openShareDialog(BuildContext context) async {
|
||||||
@@ -397,7 +522,7 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: AssetImage('assets/campo.png'),
|
image: AssetImage('assets/campone.png'),
|
||||||
fit: BoxFit.fill,
|
fit: BoxFit.fill,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -775,4 +900,4 @@ class _PlacarPageState extends State<PlacarPage> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user