904 lines
35 KiB
Dart
904 lines
35 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;
|
|
final Set<String> _appliedSyncEventIds = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
SystemChrome.setPreferredOrientations([
|
|
DeviceOrientation.landscapeRight,
|
|
DeviceOrientation.landscapeLeft,
|
|
]);
|
|
|
|
_controller = PlacarController(
|
|
gameId: widget.gameId,
|
|
myTeam: widget.myTeam,
|
|
opponentTeam: widget.opponentTeam,
|
|
onSyncAction: _onLocalControllerSync,
|
|
);
|
|
_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();
|
|
_appliedSyncEventIds.clear();
|
|
_syncSubscription = _sharingController
|
|
.listenToGameSyncOthers(_sessionId!)
|
|
.listen(
|
|
(dynamic event) {
|
|
final rows = <Map<String, dynamic>>[];
|
|
|
|
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 || _appliedSyncEventIds.contains(rowId)) {
|
|
continue;
|
|
}
|
|
|
|
rows.add(Map<String, dynamic>.from(row));
|
|
}
|
|
} else if (event is Map<String, dynamic>) {
|
|
final row = Map<String, dynamic>.from(event);
|
|
final rowId = row['id']?.toString();
|
|
if (rowId != null && !_appliedSyncEventIds.contains(rowId)) {
|
|
rows.add(row);
|
|
}
|
|
}
|
|
|
|
if (rows.isEmpty) return;
|
|
|
|
rows.sort((a, b) {
|
|
final aTime = a['created_at']?.toString();
|
|
final bTime = b['created_at']?.toString();
|
|
if (aTime == null || bTime == null) return 0;
|
|
try {
|
|
return DateTime.parse(aTime).compareTo(DateTime.parse(bTime));
|
|
} catch (_) {
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
for (final record in rows) {
|
|
final recordId = record['id']?.toString();
|
|
if (recordId == null || _appliedSyncEventIds.contains(recordId)) {
|
|
continue;
|
|
}
|
|
|
|
_appliedSyncEventIds.add(recordId);
|
|
print(
|
|
"🔄 Evento remoto recebido: ${record['action_type']} - ${record['action_data']}",
|
|
);
|
|
_applyRemoteSyncEvent(record);
|
|
}
|
|
},
|
|
onError: (error) {
|
|
print("⚠️ Erro no stream de sync: $error");
|
|
},
|
|
);
|
|
}
|
|
|
|
void _handleSyncRecords(Map<String, dynamic> record) {
|
|
// Mantido apenas como fallback, mas a escuta principal usa listenToGameSyncOthers.
|
|
_applyRemoteSyncEvent(record);
|
|
}
|
|
|
|
void _onLocalControllerSync(
|
|
String actionType,
|
|
Map<String, dynamic> actionData,
|
|
) {
|
|
if (_sessionId == null || _isApplyingRemoteSync) return;
|
|
print("📤 Enviando sync action local: $actionType -> $actionData (is_running: ${actionData['is_running']})");
|
|
_sharingController.sendSyncEvent(_sessionId!, actionType, actionData);
|
|
}
|
|
|
|
void _applyRemoteSyncEvent(Map<String, dynamic> record) {
|
|
final actionType = record['action_type']?.toString();
|
|
final actionData = Map<String, dynamic>.from(record['action_data'] ?? {});
|
|
|
|
// Aplicar estado remoto do timer 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);
|
|
}
|
|
|
|
final remoteQuarter = int.tryParse(
|
|
actionData['current_quarter']?.toString() ?? '',
|
|
);
|
|
if (remoteQuarter != null) {
|
|
_controller.currentQuarter = remoteQuarter;
|
|
}
|
|
|
|
final remoteMyFouls = int.tryParse(
|
|
actionData['my_fouls']?.toString() ?? '',
|
|
);
|
|
if (remoteMyFouls != null) {
|
|
_controller.myFouls = remoteMyFouls;
|
|
}
|
|
|
|
final remoteOpponentFouls = int.tryParse(
|
|
actionData['opponent_fouls']?.toString() ?? '',
|
|
);
|
|
if (remoteOpponentFouls != null) {
|
|
_controller.opponentFouls = remoteOpponentFouls;
|
|
}
|
|
|
|
final remoteMyTimeoutsUsed = int.tryParse(
|
|
actionData['my_timeouts_used']?.toString() ?? '',
|
|
);
|
|
if (remoteMyTimeoutsUsed != null) {
|
|
_controller.myTimeoutsUsed = remoteMyTimeoutsUsed;
|
|
}
|
|
|
|
final remoteOpponentTimeoutsUsed = int.tryParse(
|
|
actionData['opponent_timeouts_used']?.toString() ?? '',
|
|
);
|
|
if (remoteOpponentTimeoutsUsed != null) {
|
|
_controller.opponentTimeoutsUsed = remoteOpponentTimeoutsUsed;
|
|
}
|
|
|
|
if (remoteIsRunning != _controller.isRunning) {
|
|
_isApplyingRemoteSync = true;
|
|
_controller.applyRemoteTimerState(remoteIsRunning);
|
|
_controller.notifyListeners();
|
|
_isApplyingRemoteSync = false;
|
|
}
|
|
|
|
if (actionType == 'toggle_timer') {
|
|
setState(() {});
|
|
} else if (actionType == 'commit_stat') {
|
|
final action = actionData['action']?.toString() ?? '';
|
|
final playerData = actionData['player_data']?.toString() ?? '';
|
|
if (action.isNotEmpty && playerData.isNotEmpty) {
|
|
_isApplyingRemoteSync = true;
|
|
_controller.commitStat(action, playerData);
|
|
_isApplyingRemoteSync = false;
|
|
}
|
|
} else if (actionType == 'register_foul') {
|
|
final committer = actionData['committer']?.toString() ?? '';
|
|
final foulType = actionData['foulType']?.toString() ?? '';
|
|
final victim = actionData['victim']?.toString() ?? '';
|
|
if (committer.isNotEmpty && foulType.isNotEmpty) {
|
|
_isApplyingRemoteSync = true;
|
|
_controller.registerFoul(committer, foulType, victim);
|
|
_isApplyingRemoteSync = false;
|
|
}
|
|
} else if (actionType == 'subbing') {
|
|
final action = actionData['action']?.toString() ?? '';
|
|
final courtPlayer = actionData['court_player']?.toString() ?? '';
|
|
final isOpponent = actionData['is_opponent'] == true;
|
|
if (action.isNotEmpty && courtPlayer.isNotEmpty) {
|
|
_isApplyingRemoteSync = true;
|
|
_controller.handleSubbing(context, action, courtPlayer, isOpponent);
|
|
_isApplyingRemoteSync = false;
|
|
}
|
|
} else if (actionType == 'swap_players') {
|
|
final dragged = actionData['dragged']?.toString() ?? '';
|
|
final target = actionData['target']?.toString() ?? '';
|
|
if (dragged.isNotEmpty && target.isNotEmpty) {
|
|
_isApplyingRemoteSync = true;
|
|
_controller.swapCourtPlayers(dragged, target);
|
|
_isApplyingRemoteSync = false;
|
|
}
|
|
} else if (actionType == 'use_timeout') {
|
|
final isOpponent = actionData['is_opponent'] == true;
|
|
_isApplyingRemoteSync = true;
|
|
_controller.useTimeout(isOpponent);
|
|
_isApplyingRemoteSync = false;
|
|
}
|
|
}
|
|
|
|
void _handleTimerButton(BuildContext context) {
|
|
_controller.toggleTimer(context);
|
|
}
|
|
|
|
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/campone.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),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|