bora para o relatorio
This commit is contained in:
@@ -139,6 +139,36 @@ class PlacarController extends ChangeNotifier {
|
|||||||
_setTimerRunning(shouldRun, emitSync: false);
|
_setTimerRunning(shouldRun, emitSync: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void applyRemoteAddShot(Map<String, dynamic> shotJson) {
|
||||||
|
try {
|
||||||
|
matchShots.add(ShotRecord.fromJson(shotJson));
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Erro ao aplicar shot remoto: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _persistShotRemote(Map<String, dynamic> shotJson) async {
|
||||||
|
try {
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
final row = {
|
||||||
|
'game_id': gameId,
|
||||||
|
'member_id': shotJson['playerId'] ?? shotJson['player_id'],
|
||||||
|
'player_name': shotJson['playerName'] ?? shotJson['player_name'],
|
||||||
|
'relative_x': shotJson['relativeX'] ?? shotJson['relative_x'],
|
||||||
|
'relative_y': shotJson['relativeY'] ?? shotJson['relative_y'],
|
||||||
|
'is_make': shotJson['isMake'] ?? shotJson['is_make'],
|
||||||
|
'zone': shotJson['zone'],
|
||||||
|
'points': shotJson['points'],
|
||||||
|
};
|
||||||
|
|
||||||
|
await supabase.from('shot_locations').insert(row);
|
||||||
|
debugPrint('✅ Shot persisted remotely');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Erro ao persistir shot remoto: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
bool isSaving = false;
|
bool isSaving = false;
|
||||||
bool gameWasAlreadyFinished = false;
|
bool gameWasAlreadyFinished = false;
|
||||||
@@ -666,6 +696,14 @@ class PlacarController extends ChangeNotifier {
|
|||||||
|
|
||||||
String finalAction = isMake ? "add_pts_$points" : "miss_$points";
|
String finalAction = isMake ? "add_pts_$points" : "miss_$points";
|
||||||
commitStat(finalAction, targetPlayer);
|
commitStat(finalAction, targetPlayer);
|
||||||
|
// Emitir evento de shot para parceiros remotos
|
||||||
|
try {
|
||||||
|
final shotJson = matchShots.last.toJson();
|
||||||
|
_dispatchSyncAction('add_shot', {'shot': shotJson});
|
||||||
|
// Persist shot immediately on server (fire-and-forget)
|
||||||
|
_persistShotRemote(shotJson);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,6 +735,14 @@ class PlacarController extends ChangeNotifier {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Emitir evento de shot para parceiros remotos
|
||||||
|
try {
|
||||||
|
final shotJson = matchShots.last.toJson();
|
||||||
|
_dispatchSyncAction('add_shot', {'shot': shotJson});
|
||||||
|
// Persist shot immediately on server (fire-and-forget)
|
||||||
|
_persistShotRemote(shotJson);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
commitStat(pendingAction!, pendingPlayerId!);
|
commitStat(pendingAction!, pendingPlayerId!);
|
||||||
|
|
||||||
isSelectingShotLocation = false;
|
isSelectingShotLocation = false;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
StreamSubscription? _syncSubscription;
|
StreamSubscription? _syncSubscription;
|
||||||
bool _isApplyingRemoteSync = false;
|
bool _isApplyingRemoteSync = false;
|
||||||
final Set<String> _appliedSyncEventIds = {};
|
final Set<String> _appliedSyncEventIds = {};
|
||||||
|
final Map<String, DateTime> _lastAppliedActionAt = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -169,7 +170,7 @@
|
|||||||
_sharedWithName = await _resolveUserName(sharedWith);
|
_sharedWithName = await _resolveUserName(sharedWith);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupSyncListener();
|
await _setupSyncListener();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,10 +190,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setupSyncListener() {
|
Future<void> _setupSyncListener() async {
|
||||||
if (_sessionId == null) return;
|
if (_sessionId == null) return;
|
||||||
_syncSubscription?.cancel();
|
_syncSubscription?.cancel();
|
||||||
_appliedSyncEventIds.clear();
|
_appliedSyncEventIds.clear();
|
||||||
|
await _seedAppliedSyncEventIds();
|
||||||
_syncSubscription = _sharingController
|
_syncSubscription = _sharingController
|
||||||
.listenToGameSyncOthers(_sessionId!)
|
.listenToGameSyncOthers(_sessionId!)
|
||||||
.listen(
|
.listen(
|
||||||
@@ -238,7 +240,29 @@
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip if this event is older or equal to the last applied event of the same type
|
||||||
|
final actionType = record['action_type']?.toString();
|
||||||
|
DateTime? recordTime;
|
||||||
|
try {
|
||||||
|
final created = record['created_at']?.toString();
|
||||||
|
if (created != null) recordTime = DateTime.parse(created);
|
||||||
|
} catch (_) {
|
||||||
|
recordTime = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionType != null && recordTime != null) {
|
||||||
|
final last = _lastAppliedActionAt[actionType];
|
||||||
|
if (last != null && !recordTime.isAfter(last)) {
|
||||||
_appliedSyncEventIds.add(recordId);
|
_appliedSyncEventIds.add(recordId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_appliedSyncEventIds.add(recordId);
|
||||||
|
if (actionType != null && recordTime != null) {
|
||||||
|
_lastAppliedActionAt[actionType] = recordTime;
|
||||||
|
}
|
||||||
|
|
||||||
print(
|
print(
|
||||||
"🔄 Evento remoto recebido: ${record['action_type']} - ${record['action_data']}",
|
"🔄 Evento remoto recebido: ${record['action_type']} - ${record['action_data']}",
|
||||||
);
|
);
|
||||||
@@ -251,6 +275,29 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _seedAppliedSyncEventIds() async {
|
||||||
|
if (_sessionId == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await Supabase.instance.client
|
||||||
|
.from('game_sync_events')
|
||||||
|
.select('id')
|
||||||
|
.eq('session_id', _sessionId!)
|
||||||
|
.order('created_at', ascending: true);
|
||||||
|
|
||||||
|
if (response is List) {
|
||||||
|
for (final item in response) {
|
||||||
|
final id = item['id']?.toString();
|
||||||
|
if (id != null) {
|
||||||
|
_appliedSyncEventIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('⚠️ Erro ao semear eventos históricos de sync: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _handleSyncRecords(Map<String, dynamic> record) {
|
void _handleSyncRecords(Map<String, dynamic> record) {
|
||||||
// Mantido apenas como fallback, mas a escuta principal usa listenToGameSyncOthers.
|
// Mantido apenas como fallback, mas a escuta principal usa listenToGameSyncOthers.
|
||||||
_applyRemoteSyncEvent(record);
|
_applyRemoteSyncEvent(record);
|
||||||
@@ -317,7 +364,6 @@
|
|||||||
if (remoteIsRunning != _controller.isRunning) {
|
if (remoteIsRunning != _controller.isRunning) {
|
||||||
_isApplyingRemoteSync = true;
|
_isApplyingRemoteSync = true;
|
||||||
_controller.applyRemoteTimerState(remoteIsRunning);
|
_controller.applyRemoteTimerState(remoteIsRunning);
|
||||||
_controller.notifyListeners();
|
|
||||||
_isApplyingRemoteSync = false;
|
_isApplyingRemoteSync = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,6 +408,13 @@
|
|||||||
_isApplyingRemoteSync = true;
|
_isApplyingRemoteSync = true;
|
||||||
_controller.useTimeout(isOpponent);
|
_controller.useTimeout(isOpponent);
|
||||||
_isApplyingRemoteSync = false;
|
_isApplyingRemoteSync = false;
|
||||||
|
} else if (actionType == 'add_shot') {
|
||||||
|
final shot = actionData['shot'] as Map<String, dynamic>?;
|
||||||
|
if (shot != null) {
|
||||||
|
_isApplyingRemoteSync = true;
|
||||||
|
_controller.applyRemoteAddShot(Map<String, dynamic>.from(shot));
|
||||||
|
_isApplyingRemoteSync = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +436,7 @@
|
|||||||
if (result != null) {
|
if (result != null) {
|
||||||
_sessionId = result['session_id']?.toString();
|
_sessionId = result['session_id']?.toString();
|
||||||
_shareCode = result['share_code']?.toString();
|
_shareCode = result['share_code']?.toString();
|
||||||
_setupSyncListener();
|
await _setupSyncListener();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,7 +451,7 @@
|
|||||||
_sessionId = result['session_id']?.toString();
|
_sessionId = result['session_id']?.toString();
|
||||||
_shareCode = result['share_code']?.toString();
|
_shareCode = result['share_code']?.toString();
|
||||||
_sharedWithName = result['creator_name']?.toString() ?? '';
|
_sharedWithName = result['creator_name']?.toString() ?? '';
|
||||||
_setupSyncListener();
|
await _setupSyncListener();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,62 +404,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: 22.0 * context.sf,
|
horizontal: 22.0 * context.sf,
|
||||||
vertical: 16.0 * context.sf),
|
vertical: 16.0 * context.sf),
|
||||||
child: Column(
|
child: LayoutBuilder(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
builder: (context, constraints) {
|
||||||
children: [
|
final bool isWide = constraints.maxWidth >= 1100;
|
||||||
InkWell(
|
final double effectiveCardHeight =
|
||||||
onTap: () => _showTeamSelector(context),
|
isWide ? 280 * context.sf : cardHeight;
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(12 * context.sf),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardTheme.color,
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(15 * context.sf),
|
|
||||||
border:
|
|
||||||
Border.all(color: Colors.grey.withOpacity(0.2)),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(children: [
|
|
||||||
(_selectedTeamLogo != null &&
|
|
||||||
_selectedTeamLogo!.isNotEmpty)
|
|
||||||
? ClipOval(
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: _selectedTeamLogo!,
|
|
||||||
width: 24 * context.sf,
|
|
||||||
height: 24 * context.sf,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
placeholder: (context, url) => Icon(
|
|
||||||
Icons.shield,
|
|
||||||
color: AppTheme.primaryRed,
|
|
||||||
size: 24 * context.sf),
|
|
||||||
errorWidget:
|
|
||||||
(context, url, error) => Icon(
|
|
||||||
Icons.shield,
|
|
||||||
color: AppTheme.primaryRed,
|
|
||||||
size: 24 * context.sf),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Icon(Icons.shield,
|
|
||||||
color: AppTheme.primaryRed,
|
|
||||||
size: 24 * context.sf),
|
|
||||||
SizedBox(width: 10 * context.sf),
|
|
||||||
Text(_selectedTeamName,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16 * context.sf,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: textColor)),
|
|
||||||
]),
|
|
||||||
Icon(Icons.arrow_drop_down, color: textColor),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 20 * context.sf),
|
|
||||||
|
|
||||||
|
Widget statsSection = Column(
|
||||||
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: cardHeight,
|
height: effectiveCardHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -484,9 +438,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 12 * context.sf),
|
SizedBox(height: 12 * context.sf),
|
||||||
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: cardHeight,
|
height: effectiveCardHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -510,15 +463,18 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 40 * context.sf),
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget historySection = Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Text('Histórico de Jogos',
|
Text('Histórico de Jogos',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20 * context.sf,
|
fontSize: 20 * context.sf,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: textColor)),
|
color: textColor)),
|
||||||
SizedBox(height: 16 * context.sf),
|
SizedBox(height: 16 * context.sf),
|
||||||
|
|
||||||
_selectedTeamName == "Selecionar Equipa"
|
_selectedTeamName == "Selecionar Equipa"
|
||||||
? Container(
|
? Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -542,7 +498,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.all(18 * context.sf),
|
padding: EdgeInsets.all(18 * context.sf),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppTheme.primaryRed.withOpacity(0.08),
|
color: AppTheme.primaryRed
|
||||||
|
.withOpacity(0.08),
|
||||||
shape: BoxShape.circle),
|
shape: BoxShape.circle),
|
||||||
child: Icon(Icons.shield_outlined,
|
child: Icon(Icons.shield_outlined,
|
||||||
color: AppTheme.primaryRed,
|
color: AppTheme.primaryRed,
|
||||||
@@ -591,13 +548,14 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
: StreamBuilder<List<Map<String, dynamic>>>(
|
: StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: _supabase
|
stream: _supabase
|
||||||
.from('games')
|
.from('games')
|
||||||
.stream(primaryKey: ['id']).order('game_date',
|
.stream(primaryKey: ['id'])
|
||||||
ascending: false),
|
.order('game_date', ascending: false),
|
||||||
builder: (context, gameSnapshot) {
|
builder: (context, gameSnapshot) {
|
||||||
if (gameSnapshot.hasError) {
|
if (gameSnapshot.hasError) {
|
||||||
return Text("Erro: ${gameSnapshot.error}",
|
return Text(
|
||||||
style:
|
"Erro: ${gameSnapshot.error}",
|
||||||
const TextStyle(color: Colors.red));
|
style: const TextStyle(
|
||||||
|
color: Colors.red));
|
||||||
}
|
}
|
||||||
if (!gameSnapshot.hasData &&
|
if (!gameSnapshot.hasData &&
|
||||||
gameSnapshot.connectionState ==
|
gameSnapshot.connectionState ==
|
||||||
@@ -679,18 +637,103 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
myScore: myScore,
|
myScore: myScore,
|
||||||
oppScore: oppScore,
|
oppScore: oppScore,
|
||||||
date: date,
|
date: date,
|
||||||
topPts: game['top_pts_name'] ?? '---',
|
topPts:
|
||||||
topAst: game['top_ast_name'] ?? '---',
|
game['top_pts_name'] ?? '---',
|
||||||
topRbs: game['top_rbs_name'] ?? '---',
|
topAst:
|
||||||
topDef: game['top_def_name'] ?? '---',
|
game['top_ast_name'] ?? '---',
|
||||||
|
topRbs:
|
||||||
|
game['top_rbs_name'] ?? '---',
|
||||||
|
topDef:
|
||||||
|
game['top_def_name'] ?? '---',
|
||||||
mvp: game['mvp_name'] ?? '---',
|
mvp: game['mvp_name'] ?? '---',
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget mainContent = Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: () => _showTeamSelector(context),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(12 * context.sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardTheme.color,
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(15 * context.sf),
|
||||||
|
border:
|
||||||
|
Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(children: [
|
||||||
|
(_selectedTeamLogo != null &&
|
||||||
|
_selectedTeamLogo!.isNotEmpty)
|
||||||
|
? ClipOval(
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: _selectedTeamLogo!,
|
||||||
|
width: 24 * context.sf,
|
||||||
|
height: 24 * context.sf,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => Icon(
|
||||||
|
Icons.shield,
|
||||||
|
color: AppTheme.primaryRed,
|
||||||
|
size: 24 * context.sf),
|
||||||
|
errorWidget:
|
||||||
|
(context, url, error) => Icon(
|
||||||
|
Icons.shield,
|
||||||
|
color: AppTheme.primaryRed,
|
||||||
|
size: 24 * context.sf),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(Icons.shield,
|
||||||
|
color: AppTheme.primaryRed,
|
||||||
|
size: 24 * context.sf),
|
||||||
|
SizedBox(width: 10 * context.sf),
|
||||||
|
Text(_selectedTeamName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16 * context.sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textColor)),
|
||||||
|
]),
|
||||||
|
Icon(Icons.arrow_drop_down, color: textColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20 * context.sf),
|
||||||
|
if (!isWide) ...[
|
||||||
|
statsSection,
|
||||||
|
SizedBox(height: 40 * context.sf),
|
||||||
|
historySection,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isWide) {
|
||||||
|
mainContent = Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(flex: 5, child: statsSection),
|
||||||
|
SizedBox(width: 20 * context.sf),
|
||||||
|
Expanded(flex: 6, child: historySection),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
mainContent,
|
||||||
SizedBox(height: 20 * context.sf),
|
SizedBox(height: 20 * context.sf),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user