Files
PlayMaker/lib/pages/PlacarPage.dart
2026-05-11 17:22:04 +01:00

779 lines
29 KiB
Dart

import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:playmaker/icons.dart/resaltosicon.dart';
import 'package:playmaker/widgets/placar_widgets.dart'; // Mantém este import
import 'package:playmaker/widgets/share_game_dialog.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../classe/theme.dart';
import '../controllers/game_sharing_controller.dart';
import '../controllers/placar_controller.dart';
class PlacarPage extends StatefulWidget {
final String gameId, myTeam, opponentTeam;
const PlacarPage({
super.key,
required this.gameId,
required this.myTeam,
required this.opponentTeam,
});
@override
State<PlacarPage> createState() => _PlacarPageState();
}
class _PlacarPageState extends State<PlacarPage> {
late PlacarController _controller;
final GameSharingController _sharingController = GameSharingController();
String? _sessionId;
String? _shareCode;
String _sharedWithName = '';
StreamSubscription? _syncSubscription;
bool _isApplyingRemoteSync = false;
@override
void initState() {
super.initState();
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeLeft,
]);
_controller = PlacarController(
gameId: widget.gameId,
myTeam: widget.myTeam,
opponentTeam: widget.opponentTeam,
);
_controller.loadPlayers().then((_) => _initializeShareForGame());
}
@override
void dispose() {
_syncSubscription?.cancel();
_controller.dispose();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
super.dispose();
}
Widget _buildFloatingFoulBtn(
String label,
Color color,
String action,
IconData icon,
double left,
double right,
double top,
double sf,
) {
return Positioned(
top: top,
left: left > 0 ? left : null,
right: right > 0 ? right : null,
child: Draggable<String>(
data: action,
feedback: Material(
color: Colors.transparent,
child: CircleAvatar(
radius: 30 * sf,
backgroundColor: color.withOpacity(0.8),
child: Icon(icon, color: Colors.white, size: 30 * sf),
),
),
child: Column(
children: [
CircleAvatar(
radius: 27 * sf,
backgroundColor: color,
child: Icon(icon, color: Colors.white, size: 28 * sf),
),
SizedBox(height: 5 * sf),
Text(
label,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12 * sf,
),
),
],
),
),
);
}
Widget _buildCornerBtn({
required String heroTag,
required IconData icon,
required Color color,
required VoidCallback? onTap,
required double size,
bool isLoading = false,
}) {
return SizedBox(
width: size,
height: size,
child: FloatingActionButton(
heroTag: heroTag,
backgroundColor: onTap == null ? Colors.grey : color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14 * (size / 50)),
),
elevation: 5,
onPressed: isLoading ? null : onTap,
child: isLoading
? SizedBox(
width: size * 0.45,
height: size * 0.45,
child: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
),
)
: Icon(icon, color: Colors.white, size: size * 0.55),
),
);
}
void _showHeatmap(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => HeatmapDialog(
shots: _controller.matchShots,
myTeamName: _controller.myTeam,
oppTeamName: _controller.opponentTeam,
myPlayersIds: [..._controller.myCourt, ..._controller.myBench],
oppPlayersIds: [..._controller.oppCourt, ..._controller.oppBench],
playerStats: _controller.playerStats,
playerNames: _controller.playerNames,
),
);
}
Future<void> _initializeShareForGame() async {
final activeSession = await _sharingController.getActiveSessionForGame(
widget.gameId,
);
if (activeSession == null) return;
_sessionId = activeSession['id']?.toString();
_shareCode = activeSession['share_code']?.toString();
final sharedWith = activeSession['shared_with_user_id']?.toString();
if (sharedWith != null && sharedWith.isNotEmpty) {
_sharedWithName = await _resolveUserName(sharedWith);
}
_setupSyncListener();
setState(() {});
}
Future<String> _resolveUserName(String userId) async {
try {
final profile = await Supabase.instance.client
.from('profiles')
.select('username, full_name')
.eq('id', userId)
.single();
return profile['full_name']?.toString() ??
profile['username']?.toString() ??
'Parceiro';
} catch (_) {
return 'Parceiro';
}
}
void _setupSyncListener() {
if (_sessionId == null) return;
_syncSubscription?.cancel();
_syncSubscription = _sharingController.listenToGameSync(_sessionId!).listen(
(event) {
if (event is List && event.isNotEmpty) {
final record = event.last as Map<String, dynamic>?;
if (record != null) {
_handleSyncRecords(record);
}
}
},
);
}
void _handleSyncRecords(Map<String, dynamic> record) {
final triggeredBy = record['triggered_by']?.toString();
final currentUserId = Supabase.instance.client.auth.currentUser?.id;
if (triggeredBy == null || triggeredBy == currentUserId) return;
_applyRemoteSyncEvent(record);
}
void _applyRemoteSyncEvent(Map<String, dynamic> record) {
final actionType = record['action_type']?.toString();
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) {
_isApplyingRemoteSync = true;
_controller.toggleTimer(context);
_isApplyingRemoteSync = false;
} else if (!paused && !_controller.isRunning) {
_isApplyingRemoteSync = true;
_controller.toggleTimer(context);
_isApplyingRemoteSync = false;
}
setState(() {});
}
}
void _handleTimerButton(BuildContext 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 {
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (ctx) => ShareGameDialog(
gameId: widget.gameId,
controller: _sharingController,
activeSessionId: _sessionId,
activeShareCode: _shareCode,
),
);
if (result != null) {
_sessionId = result['session_id']?.toString();
_shareCode = result['share_code']?.toString();
_setupSyncListener();
setState(() {});
}
}
Future<void> _openJoinDialog(BuildContext context) async {
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (ctx) => JoinGameDialog(controller: _sharingController),
);
if (result != null) {
_sessionId = result['session_id']?.toString();
_shareCode = result['share_code']?.toString();
_sharedWithName = result['creator_name']?.toString() ?? '';
_setupSyncListener();
setState(() {});
}
}
Widget _buildShareStatus(double sf) {
if (_sessionId == null) return const SizedBox.shrink();
final text = _sharedWithName.isNotEmpty
? 'Partilhado com $_sharedWithName'
: 'Sessão partilhada: $_shareCode';
return Container(
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.55),
borderRadius: BorderRadius.circular(14 * sf),
border: Border.all(color: Colors.white24),
),
child: Text(
text,
style: TextStyle(
color: Colors.white,
fontSize: 13 * sf,
fontWeight: FontWeight.bold,
),
),
);
}
@override
Widget build(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
final double sf = math.min(wScreen / 1150, hScreen / 720);
final double cornerBtnSize = 48 * sf;
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
if (_controller.isLoading) {
return Scaffold(
backgroundColor: AppTheme.placarDarkSurface,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"PREPARANDO O PAVILHÃO",
style: TextStyle(
color: Colors.white24,
fontSize: 45 * sf,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
SizedBox(height: 35 * sf),
const CircularProgressIndicator(color: Colors.orangeAccent),
],
),
),
);
}
return Scaffold(
backgroundColor: AppTheme.placarBackground,
body: SafeArea(
top: false,
bottom: false,
child: IgnorePointer(
ignoring: _controller.isSaving,
child: Stack(
children: [
Container(
margin: EdgeInsets.only(
left: 65 * sf,
right: 65 * sf,
bottom: 55 * sf,
),
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2.5),
),
child: LayoutBuilder(
builder: (context, constraints) {
final w = constraints.maxWidth;
final h = constraints.maxHeight;
return Stack(
children: [
GestureDetector(
onTapDown: (details) {
if (_controller.isSelectingShotLocation) {
bool isMake =
_controller.pendingAction?.startsWith(
"add_pts_",
) ??
false;
String? pData = _controller.pendingPlayerId;
_controller.registerShotLocation(
context,
details.localPosition,
Size(w, h),
);
if (isMake && pData != null) {
bool isOpp = pData.startsWith(
"player_opp_",
);
String pId = pData
.replaceAll("player_my_", "")
.replaceAll("player_opp_", "");
showAssistDialog(
context,
_controller,
isOpp,
pId,
sf,
);
}
}
},
child: Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/campo.png'),
fit: BoxFit.fill,
),
),
),
),
if (!_controller.isSelectingShotLocation &&
_controller.myCourt.length >= 5 &&
_controller.oppCourt.length >= 5) ...[
Positioned(
top: h * 0.25,
left: w * 0.02,
child: PlayerCourtCard(
controller: _controller,
playerId: _controller.myCourt[0],
isOpponent: false,
sf: sf,
),
),
Positioned(
top: h * 0.68,
left: w * 0.02,
child: PlayerCourtCard(
controller: _controller,
playerId: _controller.myCourt[1],
isOpponent: false,
sf: sf,
),
),
Positioned(
top: h * 0.45,
left: w * 0.25,
child: PlayerCourtCard(
controller: _controller,
playerId: _controller.myCourt[2],
isOpponent: false,
sf: sf,
),
),
Positioned(
top: h * 0.15,
left: w * 0.20,
child: PlayerCourtCard(
controller: _controller,
playerId: _controller.myCourt[3],
isOpponent: false,
sf: sf,
),
),
Positioned(
top: h * 0.80,
left: w * 0.20,
child: PlayerCourtCard(
controller: _controller,
playerId: _controller.myCourt[4],
isOpponent: false,
sf: sf,
),
),
Positioned(
top: h * 0.25,
right: w * 0.02,
child: PlayerCourtCard(
controller: _controller,
playerId: _controller.oppCourt[0],
isOpponent: true,
sf: sf,
),
),
Positioned(
top: h * 0.68,
right: w * 0.02,
child: PlayerCourtCard(
controller: _controller,
playerId: _controller.oppCourt[1],
isOpponent: true,
sf: sf,
),
),
Positioned(
top: h * 0.45,
right: w * 0.25,
child: PlayerCourtCard(
controller: _controller,
playerId: _controller.oppCourt[2],
isOpponent: true,
sf: sf,
),
),
Positioned(
top: h * 0.15,
right: w * 0.20,
child: PlayerCourtCard(
controller: _controller,
playerId: _controller.oppCourt[3],
isOpponent: true,
sf: sf,
),
),
Positioned(
top: h * 0.80,
right: w * 0.20,
child: PlayerCourtCard(
controller: _controller,
playerId: _controller.oppCourt[4],
isOpponent: true,
sf: sf,
),
),
],
if (!_controller.isSelectingShotLocation) ...[
_buildFloatingFoulBtn(
"FALTA +",
AppTheme.actionPoints,
"add_foul",
Icons.sports,
w * 0.39,
0.0,
h * 0.31,
sf,
),
_buildFloatingFoulBtn(
"FALTA -",
AppTheme.actionMiss,
"sub_foul",
Icons.block,
0.0,
w * 0.39,
h * 0.31,
sf,
),
],
if (!_controller.isSelectingShotLocation)
Positioned(
top: (h * 0.32) + (40 * sf),
left: 0,
right: 0,
child: Center(
child: GestureDetector(
onTap: () => _handleTimerButton(context),
child: CircleAvatar(
radius: 68 * sf,
backgroundColor: Colors.grey.withOpacity(
0.5,
),
child: Icon(
_controller.isRunning
? Icons.pause
: Icons.play_arrow,
color: Colors.white,
size: 58 * sf,
),
),
),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: Center(
child: TopScoreboard(
controller: _controller,
sf: sf,
),
),
),
if (_sessionId != null)
Positioned(
top: 90 * sf,
left: 0,
right: 0,
child: Center(child: _buildShareStatus(sf)),
),
if (!_controller.isSelectingShotLocation)
Positioned(
bottom: -10 * sf,
left: 0,
right: 0,
child: ActionButtonsPanel(
controller: _controller,
sf: sf,
),
),
if (_controller.isSelectingShotLocation)
Positioned(
top: h * 0.4,
left: 0,
right: 0,
child: Center(
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 35 * sf,
vertical: 18 * sf,
),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(
11 * sf,
),
border: Border.all(
color: Colors.white,
width: 1.5 * sf,
),
),
child: Text(
"TOQUE NO CAMPO PARA MARCAR O LOCAL",
style: TextStyle(
color: Colors.white,
fontSize: 22 * sf,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
);
},
),
),
Positioned(
top: 50 * sf,
left: 12 * sf,
child: Column(
children: [
_buildCornerBtn(
heroTag: 'btn_save_exit',
icon: Icons.save_alt,
color: AppTheme.oppTeamRed,
size: cornerBtnSize,
isLoading: _controller.isSaving,
onTap: () async {
await _controller.saveGameStats(context);
if (context.mounted) Navigator.pop(context);
},
),
SizedBox(height: 10 * sf),
_buildCornerBtn(
heroTag: 'btn_history',
icon: Icons.history,
color: Colors.blueGrey,
size: cornerBtnSize,
onTap: () => showDialog(
context: context,
builder: (ctx) =>
PlayByPlayDialog(controller: _controller),
),
),
],
),
),
Positioned(
top: 50 * sf,
right: 12 * sf,
child: Column(
children: [
_buildCornerBtn(
heroTag: 'btn_heatmap',
icon: Icons.local_fire_department,
color: Colors.orange.shade800,
size: cornerBtnSize,
onTap: () => _showHeatmap(context),
),
SizedBox(height: 10 * sf),
_buildCornerBtn(
heroTag: 'btn_boxscore',
icon: Icons.table_chart,
color: Colors.indigo,
size: cornerBtnSize,
onTap: () => showDialog(
context: context,
builder: (ctx) =>
BoxScoreDialog(controller: _controller, sf: sf),
),
),
SizedBox(height: 10 * sf),
_buildCornerBtn(
heroTag: 'btn_share',
icon: Icons.share,
color: Colors.green,
size: cornerBtnSize,
onTap: () => _openShareDialog(context),
),
],
),
),
// BOTÕES INFERIORES: SUBSTITUIÇÕES E TIMEOUTS
Positioned(
bottom: 55 * sf,
left: 12 * sf,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildCornerBtn(
heroTag: 'btn_sub_home',
icon: Icons.swap_horiz,
color: AppTheme.myTeamBlue,
size: cornerBtnSize,
onTap: () => showDialog(
context: context,
builder: (ctx) => SubstitutionDialog(
controller: _controller,
isOpponent: false,
sf: sf,
),
),
),
SizedBox(height: 12 * sf),
_buildCornerBtn(
heroTag: 'btn_to_home',
icon: Icons.timer,
color: AppTheme.myTeamBlue,
size: cornerBtnSize,
onTap: _controller.myTimeoutsUsed >= 3
? null
: () => _controller.useTimeout(false),
),
],
),
),
Positioned(
bottom: 55 * sf,
right: 12 * sf,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildCornerBtn(
heroTag: 'btn_sub_away',
icon: Icons.swap_horiz,
color: AppTheme.oppTeamRed,
size: cornerBtnSize,
onTap: () => showDialog(
context: context,
builder: (ctx) => SubstitutionDialog(
controller: _controller,
isOpponent: true,
sf: sf,
),
),
),
SizedBox(height: 12 * sf),
_buildCornerBtn(
heroTag: 'btn_to_away',
icon: Icons.timer,
color: AppTheme.oppTeamRed,
size: cornerBtnSize,
onTap: _controller.opponentTimeoutsUsed >= 3
? null
: () => _controller.useTimeout(true),
),
],
),
),
if (_controller.isSaving)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.4),
child: const Center(
child: CircularProgressIndicator(color: Colors.white),
),
),
),
],
),
),
),
);
},
);
}
}