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
|
||||||
// ====================================
|
// ====================================
|
||||||
|
|||||||
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/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