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
|
||||
// ====================================
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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