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

157
SYNC_CHANGES_SUMMARY.md Normal file
View File

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

BIN
assets/campone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 MiB

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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