diff --git a/create_tables.sql b/create_tables.sql new file mode 100644 index 0000000..c076529 --- /dev/null +++ b/create_tables.sql @@ -0,0 +1,108 @@ +-- ===================================================== +-- GARANTIR QUE A TABELA PROFILES TEM TODAS AS COLUNAS +-- ===================================================== + +-- Verifica e adiciona colunas que podem estar em falta +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name='profiles' AND column_name='id' + ) THEN + CREATE TABLE profiles ( + id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + username VARCHAR(255), + full_name VARCHAR(255), + avatar_url TEXT, + selected_team_id UUID, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + END IF; + + -- Adiciona colunas em falta se necessário + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name='profiles' AND column_name='username' + ) THEN + ALTER TABLE profiles ADD COLUMN username VARCHAR(255); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name='profiles' AND column_name='full_name' + ) THEN + ALTER TABLE profiles ADD COLUMN full_name VARCHAR(255); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name='profiles' AND column_name='avatar_url' + ) THEN + ALTER TABLE profiles ADD COLUMN avatar_url TEXT; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name='profiles' AND column_name='selected_team_id' + ) THEN + ALTER TABLE profiles ADD COLUMN selected_team_id UUID; + END IF; +END +$$; + +-- ===================================================== +-- CRIAR TABELAS DE COMPARTILHAMENTO DE JOGO +-- ===================================================== + +DROP TABLE IF EXISTS game_sync_events; +DROP TABLE IF EXISTS game_sessions; + +CREATE TABLE game_sessions ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + game_id UUID NOT NULL REFERENCES games(id) ON DELETE CASCADE, + created_by UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + shared_with_user_id UUID REFERENCES profiles(id) ON DELETE SET NULL, + share_code VARCHAR(10) UNIQUE NOT NULL, + status VARCHAR(20) DEFAULT 'active', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE TABLE game_sync_events ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + session_id UUID NOT NULL REFERENCES game_sessions(id) ON DELETE CASCADE, + action_type VARCHAR(50) NOT NULL, + action_data JSONB, + triggered_by UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- ===================================================== +-- CRIAR ÍNDICES PARA PERFORMANCE +-- ===================================================== + +CREATE INDEX IF NOT EXISTS idx_game_sessions_game_id ON game_sessions(game_id); +CREATE INDEX IF NOT EXISTS idx_game_sessions_share_code ON game_sessions(share_code); +CREATE INDEX IF NOT EXISTS idx_game_sessions_status ON game_sessions(status); +CREATE INDEX IF NOT EXISTS idx_game_sync_events_session_id ON game_sync_events(session_id); + +-- ===================================================== +-- CRIAR TRIGGER AUTOMÁTICO PARA NOVOS UTILIZADORES +-- ===================================================== + +CREATE OR REPLACE FUNCTION public.create_profile_for_new_user() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO public.profiles (id, username) + VALUES (NEW.id, COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.email)) + ON CONFLICT (id) DO NOTHING; + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Drop the trigger if it exists and create it +DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; +CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.create_profile_for_new_user(); diff --git a/lib/controllers/game_sharing_controller.dart b/lib/controllers/game_sharing_controller.dart new file mode 100644 index 0000000..a9db6cb --- /dev/null +++ b/lib/controllers/game_sharing_controller.dart @@ -0,0 +1,256 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'dart:math'; + +class GameSharingController { + final _supabase = Supabase.instance.client; + + String get myUserId => _supabase.auth.currentUser?.id ?? ''; + String get myUserEmail => _supabase.auth.currentUser?.email ?? ''; + + // ==================================== + // 1️⃣ GERAR CÓDIGO E CRIAR SESSÃO + // ==================================== + + Future createShareSession(String gameId) async { + try { + final shareCode = _generateShareCode(); + + final response = await _supabase.from('game_sessions').insert({ + 'game_id': gameId, + 'created_by': myUserId, + 'share_code': shareCode, + 'status': 'active', + }).select().single(); + + return shareCode; + } catch (e) { + print("❌ Erro ao criar sessão de compartilhamento: $e"); + return null; + } + } + + // ==================================== + // 2️⃣ ENTRAR EM JOGO COMPARTILHADO + // ==================================== + + Future?> joinGameByCode(String shareCode) async { + try { + print("🔍 Procurando sessão com código: $shareCode"); + // Procura a sessão pelo código + final sessions = await _supabase + .from('game_sessions') + .select() + .eq('share_code', shareCode.toUpperCase()) + .eq('status', 'active'); + + print("📋 Sessões encontradas: ${sessions.length}"); + if (sessions.isEmpty) { + print("❌ Código inválido ou expirado"); + return null; + } + + final session = sessions.first; + final gameId = session['game_id'] as String; + final createdBy = session['created_by'] as String; + + print("🎮 Game ID: $gameId, Criado por: $createdBy"); + + // Garante que o utilizador atual tem perfil + print("👤 Verificando perfil do utilizador: $myUserId"); + await _ensureUserProfile(); + + // Atualiza a sessão para adicionar o utilizador que está a entrar + await _supabase.from('game_sessions').update({ + 'shared_with_user_id': myUserId, + 'updated_at': DateTime.now().toIso8601String(), + }).eq('id', session['id']); + + print("✅ Sessão atualizada com novo utilizador"); + + // Busca informações do jogo + final gameData = await _supabase + .from('games') + .select() + .eq('id', gameId) + .single(); + + print("📊 Dados do jogo: $gameData"); + + // Busca o nome do utilizador que criou + final creatorData = await _supabase + .from('profiles') + .select('username, full_name') + .eq('id', createdBy) + .single(); + + print("👤 Criador: ${creatorData['full_name'] ?? creatorData['username']}"); + + return { + 'session_id': session['id'], + 'game_id': gameId, + 'creator_name': creatorData['full_name'] ?? creatorData['username'] ?? 'Utilizador', + 'game': gameData, + }; + } catch (e) { + print("❌ Erro ao entrar no jogo: $e"); + return null; + } + } + + // ==================================== + // GARANTIR QUE UTILIZADOR TEM PERFIL + // ==================================== + + Future _ensureUserProfile() async { + try { + final user = _supabase.auth.currentUser; + if (user == null) return; + + // Verifica se o perfil existe + final existing = await _supabase + .from('profiles') + .select() + .eq('id', user.id) + .maybeSingle(); + + if (existing == null) { + // Cria o perfil se não existir - usa apenas colunas básicas + print("📝 Criando perfil para novo utilizador"); + await _supabase.from('profiles').upsert({ + 'id': user.id, + 'username': user.email?.split('@').first ?? 'user', + }, onConflict: 'id'); + print("✅ Perfil criado com sucesso"); + } else { + print("✅ Perfil já existe"); + } + } catch (e) { + print("⚠️ Aviso ao verificar/criar perfil: $e"); + // Não falha o join se o perfil já existe + } + } + + // ==================================== + // 3️⃣ OBTER INFORMAÇÕES DA SESSÃO + // ==================================== + + Future?> getSessionInfo(String sessionId) async { + try { + final response = await _supabase + .from('game_sessions') + .select() + .eq('id', sessionId) + .single(); + + return response; + } catch (e) { + print("❌ Erro ao buscar informações da sessão: $e"); + return null; + } + } + + // ==================================== + // 4️⃣ ENVIAR EVENTO DE SINCRONIZAÇÃO + // ==================================== + + Future sendSyncEvent( + String sessionId, + String actionType, + Map actionData, + ) async { + try { + await _supabase.from('game_sync_events').insert({ + 'session_id': sessionId, + 'action_type': actionType, + 'action_data': actionData, + 'triggered_by': myUserId, + }); + + print("✅ Evento sincronizado: $actionType"); + return true; + } catch (e) { + print("❌ Erro ao enviar evento de sincronização: $e"); + return false; + } + } + + // ==================================== + // 5️⃣ OUVIR EVENTOS EM TEMPO REAL + // ==================================== + + Stream listenToGameSync(String sessionId) { + return _supabase + .from('game_sync_events') + .stream(primaryKey: ['id']) + .eq('session_id', sessionId) + .order('created_at', ascending: false); + } + + // ==================================== + // 6️⃣ OBTER ÚLTIMOS EVENTOS + // ==================================== + + Future>> getRecentSyncEvents(String sessionId, {int limit = 10}) async { + try { + final response = await _supabase + .from('game_sync_events') + .select() + .eq('session_id', sessionId) + .order('created_at', ascending: false) + .limit(limit); + + return List>.from(response); + } catch (e) { + print("❌ Erro ao buscar eventos: $e"); + return []; + } + } + + // ==================================== + // 7️⃣ TERMINAR SESSÃO COMPARTILHADA + // ==================================== + + Future endShareSession(String sessionId) async { + try { + await _supabase + .from('game_sessions') + .update({'status': 'ended'}) + .eq('id', sessionId); + + print("✅ Sessão terminada"); + return true; + } catch (e) { + print("❌ Erro ao terminar sessão: $e"); + return false; + } + } + + // ==================================== + // 8️⃣ OBTER SESSÃO ATIVA DO JOGO + // ==================================== + + Future?> getActiveSessionForGame(String gameId) async { + try { + final response = await _supabase + .from('game_sessions') + .select() + .eq('game_id', gameId) + .eq('status', 'active') + .single(); + + return response; + } catch (e) { + return null; // Sem sessão ativa + } + } + + // ==================================== + // FUNÇÕES PRIVADAS + // ==================================== + + String _generateShareCode() { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + final random = Random(); + return List.generate(6, (index) => chars[random.nextInt(chars.length)]).join(); + } +} diff --git a/lib/pages/PlacarPage.dart b/lib/pages/PlacarPage.dart index 3a4ed8a..03c838c 100644 --- a/lib/pages/PlacarPage.dart +++ b/lib/pages/PlacarPage.dart @@ -1,20 +1,25 @@ +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 'dart:math' as math; -import '../classe/theme.dart'; -import '../controllers/placar_controller.dart'; -import 'package:playmaker/zone_map_dialog.dart'; +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 + super.key, + required this.gameId, + required this.myTeam, + required this.opponentTeam, }); @override @@ -23,6 +28,12 @@ class PlacarPage extends StatefulWidget { class _PlacarPageState extends State { late PlacarController _controller; + final GameSharingController _sharingController = GameSharingController(); + String? _sessionId; + String? _shareCode; + String _sharedWithName = ''; + StreamSubscription? _syncSubscription; + bool _isApplyingRemoteSync = false; @override void initState() { @@ -31,23 +42,33 @@ class _PlacarPageState extends State { DeviceOrientation.landscapeRight, DeviceOrientation.landscapeLeft, ]); - + _controller = PlacarController( gameId: widget.gameId, - myTeam: widget.myTeam, + myTeam: widget.myTeam, opponentTeam: widget.opponentTeam, ); - _controller.loadPlayers(); + _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) { + 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, @@ -57,39 +78,62 @@ class _PlacarPageState extends State { feedback: Material( color: Colors.transparent, child: CircleAvatar( - radius: 30 * sf, - backgroundColor: color.withOpacity(0.8), - child: Icon(icon, color: Colors.white, size: 30 * sf) + radius: 30 * sf, + backgroundColor: color.withOpacity(0.8), + child: Icon(icon, color: Colors.white, size: 30 * sf), ), ), child: Column( children: [ CircleAvatar( - radius: 27 * sf, + radius: 27 * sf, backgroundColor: color, - child: Icon(icon, color: Colors.white, size: 28 * sf), + 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, + ), ), - 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}) { + Widget _buildCornerBtn({ + required String heroTag, + required IconData icon, + required Color color, + required VoidCallback? onTap, + required double size, + bool isLoading = false, + }) { return SizedBox( - width: size, + width: size, height: size, child: FloatingActionButton( - heroTag: heroTag, + heroTag: heroTag, backgroundColor: onTap == null ? Colors.grey : color, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14 * (size / 50))), - elevation: 5, + 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), + 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), ), ); } @@ -109,25 +153,183 @@ class _PlacarPageState extends State { ); } + Future _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 _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?; + if (record != null) { + _handleSyncRecords(record); + } + } + }, + ); + } + + void _handleSyncRecords(Map 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 record) { + final actionType = record['action_type']?.toString(); + final actionData = Map.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 _openShareDialog(BuildContext context) async { + final result = await showDialog>( + 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 _openJoinDialog(BuildContext context) async { + final result = await showDialog>( + 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; + 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, + 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), + 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), ], ), @@ -136,16 +338,23 @@ class _PlacarPageState extends State { } return Scaffold( - backgroundColor: AppTheme.placarBackground, + backgroundColor: AppTheme.placarBackground, body: SafeArea( - top: false, bottom: false, + top: false, + bottom: false, child: IgnorePointer( - ignoring: _controller.isSaving, + 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)), + 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; @@ -155,70 +364,254 @@ class _PlacarPageState extends State { GestureDetector( onTapDown: (details) { if (_controller.isSelectingShotLocation) { - bool isMake = _controller.pendingAction?.startsWith("add_pts_") ?? false; + bool isMake = + _controller.pendingAction?.startsWith( + "add_pts_", + ) ?? + false; String? pData = _controller.pendingPlayerId; - - _controller.registerShotLocation(context, details.localPosition, Size(w, h)); - + + _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); + 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: () => _controller.toggleTimer(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) - ), + image: AssetImage('assets/campo.png'), + fit: BoxFit.fill, ), ), ), - Positioned(top: 0, left: 0, right: 0, child: Center(child: TopScoreboard(controller: _controller, sf: sf))), - - if (!_controller.isSelectingShotLocation) Positioned(bottom: -10 * sf, left: 0, right: 0, child: ActionButtonsPanel(controller: _controller, sf: sf)), + ), + 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, + 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)), + 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, + ), + ), ), ), ), @@ -229,30 +622,77 @@ class _PlacarPageState extends State { ), Positioned( - top: 50 * sf, left: 12 * sf, + 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); }), + _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))), + _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, + 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)), + _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))), + _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, + bottom: 55 * sf, + left: 12 * sf, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -270,14 +710,23 @@ class _PlacarPageState extends State { ), ), ), - 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)), + 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, + bottom: 55 * sf, + right: 12 * sf, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -295,14 +744,29 @@ class _PlacarPageState extends State { ), ), ), - 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)), + 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)))), + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.4), + child: const Center( + child: CircularProgressIndicator(color: Colors.white), + ), + ), + ), ], ), ), @@ -311,4 +775,4 @@ class _PlacarPageState extends State { }, ); } -} \ No newline at end of file +} diff --git a/lib/pages/gamePage.dart b/lib/pages/gamePage.dart index c678ecf..c28e420 100644 --- a/lib/pages/gamePage.dart +++ b/lib/pages/gamePage.dart @@ -1,11 +1,12 @@ -import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/game_model.dart'; import 'package:flutter/material.dart'; import 'package:playmaker/pages/PlacarPage.dart'; +import 'package:playmaker/widgets/share_game_dialog.dart'; import 'package:playmaker/classe/theme.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../controllers/team_controller.dart'; import '../controllers/game_controller.dart'; +import '../controllers/game_sharing_controller.dart'; import '../utils/size_extension.dart'; import 'pdf_export_service.dart'; @@ -281,6 +282,7 @@ class _CreateGameDialogManualState extends State { late TextEditingController _seasonController; final TextEditingController _myTeamController = TextEditingController(); final TextEditingController _opponentController = TextEditingController(); + final GameSharingController _sharingController = GameSharingController(); bool _isLoading = false; @override @@ -318,6 +320,7 @@ class _CreateGameDialogManualState extends State { ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf, color: Colors.grey))), + TextButton(onPressed: _isLoading ? null : () async => await _joinRoom(), child: Text('ENTRAR NA SALA', style: TextStyle(fontSize: 14 * widget.sf))), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * widget.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)), onPressed: _isLoading ? null : () async { @@ -337,6 +340,40 @@ class _CreateGameDialogManualState extends State { ); } + Future _joinRoom() async { + print("🚪 Abrindo diálogo para entrar na sala"); + final result = await showDialog>( + context: context, + builder: (ctx) => JoinGameDialog(controller: _sharingController), + ); + + print("📦 Resultado do diálogo: $result"); + if (result == null) { + print("❌ Resultado nulo, cancelado"); + return; + } + final gameData = result['game'] as Map?; + print("🎮 Game data: $gameData"); + if (gameData == null) { + print("❌ Game data nula"); + return; + } + + final String gameId = gameData['id']?.toString() ?? ''; + final String myTeam = gameData['my_team']?.toString() ?? _myTeamController.text; + final String opponentTeam = gameData['opponent_team']?.toString() ?? _opponentController.text; + + print("🆔 Game ID: $gameId, My Team: $myTeam, Opponent: $opponentTeam"); + + if (gameId.isNotEmpty && context.mounted) { + print("➡️ Navegando para PlacarPage"); + Navigator.pop(context); + Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: gameId, myTeam: myTeam, opponentTeam: opponentTeam))); + } else { + print("❌ Game ID vazio ou contexto não montado"); + } + } + Widget _buildSearch(BuildContext context, String label, TextEditingController controller) { return StreamBuilder>>( stream: widget.teamController.teamsStream, diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 70278b0..d4d672e 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:playmaker/classe/theme.dart'; -import 'package:playmaker/grafico%20de%20pizza/grafico.dart'; +import 'package:playmaker/classe/theme.dart'; +import 'package:playmaker/grafico%20de%20pizza/grafico.dart'; import 'package:playmaker/pages/gamePage.dart'; import 'package:playmaker/pages/teamPage.dart'; -import 'package:playmaker/controllers/team_controller.dart'; +import 'package:playmaker/controllers/team_controller.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:playmaker/pages/status_page.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:playmaker/pages/status_page.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../utils/size_extension.dart'; import 'settings_screen.dart'; @@ -20,9 +20,10 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { int _selectedIndex = 0; - final TeamController _teamController = TeamController(); - String? _selectedTeamId; - String _selectedTeamName = "Selecionar Equipa"; + final TeamController _teamController = TeamController(); + + String? _selectedTeamId; + String _selectedTeamName = "Selecionar Equipa"; String? _selectedTeamLogo; int _teamWins = 0; @@ -30,55 +31,82 @@ class _HomeScreenState extends State { int _teamDraws = 0; final _supabase = Supabase.instance.client; - - String? _avatarUrl; - bool _isMemoryLoaded = false; - // A chave mágica para forçar a StatusPage a atualizar - String _statusKey = 'status_page_inicial'; + String? _avatarUrl; + bool _isMemoryLoaded = false; @override void initState() { super.initState(); - _loadUserAvatar(); - _loadSelectedTeam(); + _loadUserAvatar(); + _loadSelectedTeam(); + } + + String _prefsKey(String key) { + final userId = _supabase.auth.currentUser?.id ?? 'guest'; + return '${key}_$userId'; } Future _loadSelectedTeam() async { + // 1. Carrega primeiro do SharedPreferences para resposta imediata final prefs = await SharedPreferences.getInstance(); - final savedId = prefs.getString('last_team_id'); - + final savedId = prefs.getString(_prefsKey('last_team_id')); + if (savedId != null && mounted) { setState(() { _selectedTeamId = savedId; - _selectedTeamName = prefs.getString('last_team_name') ?? "Selecionar Equipa"; - _selectedTeamLogo = prefs.getString('last_team_logo'); - _teamWins = prefs.getInt('last_team_wins') ?? 0; - _teamLosses = prefs.getInt('last_team_losses') ?? 0; - _teamDraws = prefs.getInt('last_team_draws') ?? 0; + _selectedTeamName = + prefs.getString(_prefsKey('last_team_name')) ?? "Selecionar Equipa"; + _selectedTeamLogo = prefs.getString(_prefsKey('last_team_logo')); + _teamWins = prefs.getInt(_prefsKey('last_team_wins')) ?? 0; + _teamLosses = prefs.getInt(_prefsKey('last_team_losses')) ?? 0; + _teamDraws = prefs.getInt(_prefsKey('last_team_draws')) ?? 0; }); } + // 2. Depois sincroniza com o Supabase para ter os dados mais recentes final userId = _supabase.auth.currentUser?.id; if (userId == null) return; try { - final profile = await _supabase.from('profiles').select('selected_team_id').eq('id', userId).maybeSingle(); + final profile = await _supabase + .from('profiles') + .select('selected_team_id') + .eq('id', userId) + .maybeSingle(); if (profile != null && profile['selected_team_id'] != null) { final dbTeamId = profile['selected_team_id'].toString(); - final teamData = await _supabase.from('teams').select().eq('id', dbTeamId).maybeSingle(); + final teamData = await _supabase + .from('teams') + .select() + .eq('id', dbTeamId) + .maybeSingle(); if (teamData != null && mounted) { setState(() { _selectedTeamId = teamData['id'].toString(); _selectedTeamName = teamData['name'] ?? 'Desconhecido'; _selectedTeamLogo = teamData['image_url']; - _teamWins = int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0; - _teamLosses = int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0; - _teamDraws = int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0; + _teamWins = + int.tryParse(teamData['wins']?.toString() ?? '0') ?? 0; + _teamLosses = + int.tryParse(teamData['losses']?.toString() ?? '0') ?? 0; + _teamDraws = + int.tryParse(teamData['draws']?.toString() ?? '0') ?? 0; }); await _saveToSharedPreferences(); + } else if (mounted) { + // Se o utilizador não tem equipa selecionada, limpa valores locais para não mostrar dados de outra conta. + setState(() { + _selectedTeamId = null; + _selectedTeamName = 'Selecionar Equipa'; + _selectedTeamLogo = null; + _teamWins = 0; + _teamLosses = 0; + _teamDraws = 0; + }); + await _clearSelectedTeamFromPreferences(); } } } catch (e) { @@ -105,27 +133,37 @@ class _HomeScreenState extends State { Future _saveToSharedPreferences() async { final prefs = await SharedPreferences.getInstance(); if (_selectedTeamId != null) { - await prefs.setString('last_team_id', _selectedTeamId!); - await prefs.setString('last_team_name', _selectedTeamName); + await prefs.setString(_prefsKey('last_team_id'), _selectedTeamId!); + await prefs.setString(_prefsKey('last_team_name'), _selectedTeamName); if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) { - await prefs.setString('last_team_logo', _selectedTeamLogo!); + await prefs.setString(_prefsKey('last_team_logo'), _selectedTeamLogo!); } else { - await prefs.remove('last_team_logo'); + await prefs.remove(_prefsKey('last_team_logo')); } - await prefs.setInt('last_team_wins', _teamWins); - await prefs.setInt('last_team_losses', _teamLosses); - await prefs.setInt('last_team_draws', _teamDraws); + await prefs.setInt(_prefsKey('last_team_wins'), _teamWins); + await prefs.setInt(_prefsKey('last_team_losses'), _teamLosses); + await prefs.setInt(_prefsKey('last_team_draws'), _teamDraws); } } + Future _clearSelectedTeamFromPreferences() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_prefsKey('last_team_id')); + await prefs.remove(_prefsKey('last_team_name')); + await prefs.remove(_prefsKey('last_team_logo')); + await prefs.remove(_prefsKey('last_team_wins')); + await prefs.remove(_prefsKey('last_team_losses')); + await prefs.remove(_prefsKey('last_team_draws')); + } + Future _loadUserAvatar() async { final prefs = await SharedPreferences.getInstance(); - final savedUrl = prefs.getString('meu_avatar_guardado'); - + final savedUrl = prefs.getString(_prefsKey('meu_avatar_guardado')); + if (mounted) { setState(() { if (savedUrl != null) _avatarUrl = savedUrl; - _isMemoryLoaded = true; + _isMemoryLoaded = true; }); } @@ -133,12 +171,18 @@ class _HomeScreenState extends State { if (userId == null) return; try { - final data = await _supabase.from('profiles').select('avatar_url').eq('id', userId).maybeSingle(); + final data = await _supabase + .from('profiles') + .select('avatar_url') + .eq('id', userId) + .maybeSingle(); if (mounted && data != null && data['avatar_url'] != null) { final urlDoSupabase = data['avatar_url']; if (urlDoSupabase != savedUrl) { - await prefs.setString('meu_avatar_guardado', urlDoSupabase); - setState(() { _avatarUrl = urlDoSupabase; }); + await prefs.setString(_prefsKey('meu_avatar_guardado'), urlDoSupabase); + setState(() { + _avatarUrl = urlDoSupabase; + }); } } } catch (e) { @@ -148,17 +192,26 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { + // A StatusPage recebe a equipa selecionada diretamente como parâmetro. + // Quando _selectedTeamId muda aqui, o didUpdateWidget da StatusPage + // atualiza automaticamente — sem precisar de ValueKey nem rebuild forçado. final List pages = [ - _buildHomeContent(context), + _buildHomeContent(context), const GamePage(), - const TeamsPage(), - StatusPage(key: ValueKey(_statusKey)), // A StatusPage recarrega sempre que a chave muda! + const TeamsPage(), + StatusPage( + initialTeamId: _selectedTeamId, + initialTeamName: _selectedTeamName, + initialTeamLogo: _selectedTeamLogo, + ), ]; return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( - title: Text('PlayMaker', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold)), + title: Text('PlayMaker', + style: TextStyle( + fontSize: 20 * context.sf, fontWeight: FontWeight.bold)), backgroundColor: AppTheme.primaryRed, foregroundColor: Colors.white, elevation: 0, @@ -167,20 +220,37 @@ class _HomeScreenState extends State { child: InkWell( borderRadius: BorderRadius.circular(100), onTap: () async { - await Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen())); - _loadUserAvatar(); + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SettingsScreen()), + ); + _loadUserAvatar(); }, child: !_isMemoryLoaded - ? CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)) - : _avatarUrl != null && _avatarUrl!.isNotEmpty + ? CircleAvatar( + backgroundColor: Colors.white.withOpacity(0.2)) + : _avatarUrl != null && _avatarUrl!.isNotEmpty ? CachedNetworkImage( imageUrl: _avatarUrl!, - fadeInDuration: Duration.zero, - imageBuilder: (context, imageProvider) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), backgroundImage: imageProvider), - placeholder: (context, url) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)), - errorWidget: (context, url, error) => CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf)), + fadeInDuration: Duration.zero, + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundColor: Colors.white.withOpacity(0.2), + backgroundImage: imageProvider, + ), + placeholder: (context, url) => CircleAvatar( + backgroundColor: Colors.white.withOpacity(0.2)), + errorWidget: (context, url, error) => CircleAvatar( + backgroundColor: Colors.white.withOpacity(0.2), + child: Icon(Icons.person, + color: Colors.white, size: 20 * context.sf), + ), ) - : CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2), child: Icon(Icons.person, color: Colors.white, size: 20 * context.sf)), + : CircleAvatar( + backgroundColor: Colors.white.withOpacity(0.2), + child: Icon(Icons.person, + color: Colors.white, size: 20 * context.sf), + ), ), ), ), @@ -190,18 +260,30 @@ class _HomeScreenState extends State { onDestinationSelected: (index) { setState(() => _selectedIndex = index); if (index == 0) { - _loadSelectedTeam(); + _loadSelectedTeam(); } }, backgroundColor: Theme.of(context).colorScheme.surface, surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, elevation: 1, - height: 70 * (context.sf < 1.2 ? context.sf : 1.2), + height: 70 * (context.sf < 1.2 ? context.sf : 1.2), destinations: const [ - NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'), - NavigationDestination(icon: Icon(Icons.sports_soccer_outlined), selectedIcon: Icon(Icons.sports_soccer), label: 'Jogo'), - NavigationDestination(icon: Icon(Icons.people_outline), selectedIcon: Icon(Icons.people), label: 'Equipas'), - NavigationDestination(icon: Icon(Icons.insights_outlined), selectedIcon: Icon(Icons.insights), label: 'Status'), + NavigationDestination( + icon: Icon(Icons.home_outlined), + selectedIcon: Icon(Icons.home_filled), + label: 'Home'), + NavigationDestination( + icon: Icon(Icons.sports_soccer_outlined), + selectedIcon: Icon(Icons.sports_soccer), + label: 'Jogo'), + NavigationDestination( + icon: Icon(Icons.people_outline), + selectedIcon: Icon(Icons.people), + label: 'Equipas'), + NavigationDestination( + icon: Icon(Icons.insights_outlined), + selectedIcon: Icon(Icons.insights), + label: 'Status'), ], ), ); @@ -211,13 +293,30 @@ class _HomeScreenState extends State { showModalBottomSheet( context: context, backgroundColor: Theme.of(context).colorScheme.surface, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * context.sf))), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical(top: Radius.circular(20 * context.sf)), + ), builder: (context) { return StreamBuilder>>( stream: _teamController.teamsStream, builder: (context, snapshot) { - if (!snapshot.hasData && snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); - if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)))); + if (!snapshot.hasData && + snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator())); + } + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return SizedBox( + height: 200 * context.sf, + child: Center( + child: Text("Nenhuma equipa criada.", + style: TextStyle( + color: + Theme.of(context).colorScheme.onSurface))), + ); + } final teams = snapshot.data!; return ListView.builder( @@ -230,26 +329,49 @@ class _HomeScreenState extends State { return ListTile( leading: ClipOval( child: Container( - width: 36 * context.sf, height: 36 * context.sf, color: AppTheme.primaryRed.withOpacity(0.1), + width: 36 * context.sf, + height: 36 * context.sf, + color: AppTheme.primaryRed.withOpacity(0.1), child: (logoUrl != null && logoUrl.isNotEmpty) - ? CachedNetworkImage(imageUrl: logoUrl, fit: BoxFit.cover, placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf)) - : Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), + ? CachedNetworkImage( + imageUrl: logoUrl, + fit: BoxFit.cover, + placeholder: (context, url) => Icon( + Icons.shield, + color: AppTheme.primaryRed, + size: 20 * context.sf), + errorWidget: (context, url, error) => Icon( + Icons.shield, + color: AppTheme.primaryRed, + size: 20 * context.sf), + ) + : Icon(Icons.shield, + color: AppTheme.primaryRed, + size: 20 * context.sf), ), ), - title: Text(team['name'] ?? 'Sem Nome', style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)), + title: Text( + team['name'] ?? 'Sem Nome', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold), + ), onTap: () async { setState(() { _selectedTeamId = team['id'].toString(); _selectedTeamName = team['name'] ?? 'Desconhecido'; - _selectedTeamLogo = logoUrl; - _teamWins = int.tryParse(team['wins']?.toString() ?? '0') ?? 0; - _teamLosses = int.tryParse(team['losses']?.toString() ?? '0') ?? 0; - _teamDraws = int.tryParse(team['draws']?.toString() ?? '0') ?? 0; - - // Dizemos à StatusPage que a equipa mudou alterando a chave! - _statusKey = DateTime.now().toString(); + _selectedTeamLogo = logoUrl; + _teamWins = int.tryParse( + team['wins']?.toString() ?? '0') ?? + 0; + _teamLosses = int.tryParse( + team['losses']?.toString() ?? '0') ?? + 0; + _teamDraws = int.tryParse( + team['draws']?.toString() ?? '0') ?? + 0; }); - + await _saveSelectedTeam(); if (context.mounted) Navigator.pop(context); }, @@ -264,19 +386,24 @@ class _HomeScreenState extends State { Widget _buildHomeContent(BuildContext context) { final double wScreen = MediaQuery.of(context).size.width; - final double cardHeight = wScreen * 0.5; - final textColor = Theme.of(context).colorScheme.onSurface; + final double cardHeight = wScreen * 0.5; + final textColor = Theme.of(context).colorScheme.onSurface; return StreamBuilder>>( - stream: _selectedTeamId != null - ? _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!) + stream: _selectedTeamId != null + ? _supabase + .from('player_stats_with_names') + .stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!) : const Stream.empty(), builder: (context, snapshot) { - Map leaders = _calculateLeaders(snapshot.data ?? []); + Map leaders = + _calculateLeaders(snapshot.data ?? []); return SingleChildScrollView( child: Padding( - padding: EdgeInsets.symmetric(horizontal: 22.0 * context.sf, vertical: 16.0 * context.sf), + padding: EdgeInsets.symmetric( + horizontal: 22.0 * context.sf, + vertical: 16.0 * context.sf), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -284,16 +411,45 @@ class _HomeScreenState extends State { 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))), + 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)) + (_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), ], @@ -306,9 +462,24 @@ class _HomeScreenState extends State { height: cardHeight, child: Row( children: [ - Expanded(child: _buildStatCard(context: context, title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statPtsBg, isHighlighted: true)), + Expanded( + child: _buildStatCard( + context: context, + title: 'Mais Pontos', + playerName: leaders['pts_name'], + statValue: leaders['pts_val'].toString(), + statLabel: 'TOTAL', + color: AppTheme.statPtsBg, + isHighlighted: true)), SizedBox(width: 12 * context.sf), - Expanded(child: _buildStatCard(context: context, title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statAstBg)), + Expanded( + child: _buildStatCard( + context: context, + title: 'Assistências', + playerName: leaders['ast_name'], + statValue: leaders['ast_val'].toString(), + statLabel: 'TOTAL', + color: AppTheme.statAstBg)), ], ), ), @@ -318,64 +489,207 @@ class _HomeScreenState extends State { height: cardHeight, child: Row( children: [ - Expanded(child: _buildStatCard(context: context, title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: AppTheme.statRebBg)), + Expanded( + child: _buildStatCard( + context: context, + title: 'Rebotes', + playerName: leaders['rbs_name'], + statValue: leaders['rbs_val'].toString(), + statLabel: 'TOTAL', + color: AppTheme.statRebBg)), SizedBox(width: 12 * context.sf), - Expanded(child: PieChartCard(victories: _teamWins, defeats: _teamLosses, draws: _teamDraws, title: 'DESEMPENHO', subtitle: 'Temporada', backgroundColor: AppTheme.statPieBg, sf: context.sf)), + Expanded( + child: PieChartCard( + victories: _teamWins, + defeats: _teamLosses, + draws: _teamDraws, + title: 'DESEMPENHO', + subtitle: 'Temporada', + backgroundColor: AppTheme.statPieBg, + sf: context.sf)), ], ), ), SizedBox(height: 40 * context.sf), - - Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * context.sf, fontWeight: FontWeight.bold, color: textColor)), + + Text('Histórico de Jogos', + style: TextStyle( + fontSize: 20 * context.sf, + fontWeight: FontWeight.bold, + color: textColor)), SizedBox(height: 16 * context.sf), - - _selectedTeamName == "Selecionar Equipa" - ? Container( - width: double.infinity, padding: EdgeInsets.all(24.0 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color ?? Colors.white, borderRadius: BorderRadius.circular(16 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4))]), - child: Column( - children: [ - Container(padding: EdgeInsets.all(18 * context.sf), decoration: BoxDecoration(color: AppTheme.primaryRed.withOpacity(0.08), shape: BoxShape.circle), child: Icon(Icons.shield_outlined, color: AppTheme.primaryRed, size: 42 * context.sf)), - SizedBox(height: 20 * context.sf), - Text("Nenhuma Equipa Ativa", style: TextStyle(fontSize: 18 * context.sf, fontWeight: FontWeight.bold, color: textColor)), - SizedBox(height: 8 * context.sf), - Text("Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.", textAlign: TextAlign.center, style: TextStyle(fontSize: 13 * context.sf, color: Colors.grey.shade600, height: 1.4)), - SizedBox(height: 24 * context.sf), - SizedBox(width: double.infinity, height: 48 * context.sf, child: ElevatedButton.icon(onPressed: () => _showTeamSelector(context), style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryRed, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * context.sf))), icon: Icon(Icons.touch_app, size: 20 * context.sf), label: Text("Selecionar Agora", style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.bold)))), - ], + + _selectedTeamName == "Selecionar Equipa" + ? Container( + width: double.infinity, + padding: EdgeInsets.all(24.0 * context.sf), + decoration: BoxDecoration( + color: Theme.of(context).cardTheme.color ?? + Colors.white, + borderRadius: + BorderRadius.circular(16 * context.sf), + border: Border.all( + color: Colors.grey.withOpacity(0.1)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 4)) + ], + ), + child: Column( + children: [ + Container( + padding: EdgeInsets.all(18 * context.sf), + decoration: BoxDecoration( + color: AppTheme.primaryRed.withOpacity(0.08), + shape: BoxShape.circle), + child: Icon(Icons.shield_outlined, + color: AppTheme.primaryRed, + size: 42 * context.sf), + ), + SizedBox(height: 20 * context.sf), + Text("Nenhuma Equipa Ativa", + style: TextStyle( + fontSize: 18 * context.sf, + fontWeight: FontWeight.bold, + color: textColor)), + SizedBox(height: 8 * context.sf), + Text( + "Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13 * context.sf, + color: Colors.grey.shade600, + height: 1.4), + ), + SizedBox(height: 24 * context.sf), + SizedBox( + width: double.infinity, + height: 48 * context.sf, + child: ElevatedButton.icon( + onPressed: () => _showTeamSelector(context), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryRed, + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 10 * context.sf)), + ), + icon: Icon(Icons.touch_app, + size: 20 * context.sf), + label: Text("Selecionar Agora", + style: TextStyle( + fontSize: 15 * context.sf, + fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ) + : StreamBuilder>>( + stream: _supabase + .from('games') + .stream(primaryKey: ['id']).order('game_date', + ascending: false), + builder: (context, gameSnapshot) { + if (gameSnapshot.hasError) { + return Text("Erro: ${gameSnapshot.error}", + style: + const TextStyle(color: Colors.red)); + } + if (!gameSnapshot.hasData && + gameSnapshot.connectionState == + ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator()); + } + + final todosOsJogos = gameSnapshot.data ?? []; + final gamesList = todosOsJogos.where((game) { + String myT = + game['my_team']?.toString() ?? ''; + String oppT = + game['opponent_team']?.toString() ?? ''; + String status = + game['status']?.toString() ?? ''; + return (myT == _selectedTeamName || + oppT == _selectedTeamName) && + status == 'Terminado'; + }).take(3).toList(); + + if (gamesList.isEmpty) { + return Container( + width: double.infinity, + padding: EdgeInsets.all(20 * context.sf), + decoration: BoxDecoration( + color: Theme.of(context).cardTheme.color, + borderRadius: BorderRadius.circular(14), + ), + alignment: Alignment.center, + child: const Text( + "Ainda não há jogos terminados.", + style: TextStyle(color: Colors.grey)), + ); + } + + return Column( + children: gamesList.map((game) { + String dbMyTeam = + game['my_team']?.toString() ?? ''; + String dbOppTeam = + game['opponent_team']?.toString() ?? ''; + int dbMyScore = int.tryParse( + game['my_score']?.toString() ?? + '0') ?? + 0; + int dbOppScore = int.tryParse( + game['opponent_score'] + ?.toString() ?? + '0') ?? + 0; + String opponent; + int myScore; + int oppScore; + + if (dbMyTeam == _selectedTeamName) { + opponent = dbOppTeam; + myScore = dbMyScore; + oppScore = dbOppScore; + } else { + opponent = dbMyTeam; + myScore = dbOppScore; + oppScore = dbMyScore; + } + + String rawDate = + game['game_date']?.toString() ?? '---'; + String date = rawDate.length >= 10 + ? rawDate.substring(0, 10) + : rawDate; + String result = myScore > oppScore + ? 'V' + : (myScore < oppScore ? 'D' : 'E'); + + return _buildGameHistoryCard( + context: context, + opponent: opponent, + result: result, + myScore: myScore, + oppScore: oppScore, + date: date, + topPts: game['top_pts_name'] ?? '---', + topAst: game['top_ast_name'] ?? '---', + topRbs: game['top_rbs_name'] ?? '---', + topDef: game['top_def_name'] ?? '---', + mvp: game['mvp_name'] ?? '---', + ); + }).toList(), + ); + }, ), - ) - : StreamBuilder>>( - stream: _supabase.from('games').stream(primaryKey: ['id']).order('game_date', ascending: false), - builder: (context, gameSnapshot) { - if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red)); - if (!gameSnapshot.hasData && gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator()); - - final todosOsJogos = gameSnapshot.data ?? []; - final gamesList = todosOsJogos.where((game) { - String myT = game['my_team']?.toString() ?? ''; - String oppT = game['opponent_team']?.toString() ?? ''; - String status = game['status']?.toString() ?? ''; - return (myT == _selectedTeamName || oppT == _selectedTeamName) && status == 'Terminado'; - }).take(3).toList(); - - if (gamesList.isEmpty) return Container(width: double.infinity, padding: EdgeInsets.all(20 * context.sf), decoration: BoxDecoration(color: Theme.of(context).cardTheme.color, borderRadius: BorderRadius.circular(14)), alignment: Alignment.center, child: const Text("Ainda não há jogos terminados.", style: TextStyle(color: Colors.grey))); - - return Column( - children: gamesList.map((game) { - String dbMyTeam = game['my_team']?.toString() ?? ''; String dbOppTeam = game['opponent_team']?.toString() ?? ''; - int dbMyScore = int.tryParse(game['my_score']?.toString() ?? '0') ?? 0; int dbOppScore = int.tryParse(game['opponent_score']?.toString() ?? '0') ?? 0; - String opponent; int myScore; int oppScore; - - if (dbMyTeam == _selectedTeamName) { opponent = dbOppTeam; myScore = dbMyScore; oppScore = dbOppScore; } else { opponent = dbMyTeam; myScore = dbOppScore; oppScore = dbMyScore; } - String rawDate = game['game_date']?.toString() ?? '---'; String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate; - String result = myScore > oppScore ? 'V' : (myScore < oppScore ? 'D' : 'E'); - - return _buildGameHistoryCard(context: context, opponent: opponent, result: result, myScore: myScore, oppScore: oppScore, date: date, topPts: game['top_pts_name'] ?? '---', topAst: game['top_ast_name'] ?? '---', topRbs: game['top_rbs_name'] ?? '---', topDef: game['top_def_name'] ?? '---', mvp: game['mvp_name'] ?? '---'); - }).toList(), - ); - }, - ), - SizedBox(height: 20 * context.sf), + SizedBox(height: 20 * context.sf), ], ), ), @@ -385,78 +699,271 @@ class _HomeScreenState extends State { } Map _calculateLeaders(List> data) { - Map ptsMap = {}; Map astMap = {}; Map rbsMap = {}; Map namesMap = {}; + Map ptsMap = {}; + Map astMap = {}; + Map rbsMap = {}; + Map namesMap = {}; + for (var row in data) { - String pid = row['member_id']?.toString() ?? "unknown"; + String pid = row['member_id']?.toString() ?? "unknown"; namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido"; - ptsMap[pid] = (ptsMap[pid] ?? 0) + (int.tryParse(row['pts']?.toString() ?? '0') ?? 0); - astMap[pid] = (astMap[pid] ?? 0) + (int.tryParse(row['ast']?.toString() ?? '0') ?? 0); - rbsMap[pid] = (rbsMap[pid] ?? 0) + (int.tryParse(row['rbs']?.toString() ?? '0') ?? 0); + ptsMap[pid] = (ptsMap[pid] ?? 0) + + (int.tryParse(row['pts']?.toString() ?? '0') ?? 0); + astMap[pid] = (astMap[pid] ?? 0) + + (int.tryParse(row['ast']?.toString() ?? '0') ?? 0); + rbsMap[pid] = (rbsMap[pid] ?? 0) + + (int.tryParse(row['rbs']?.toString() ?? '0') ?? 0); } - if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0}; - String getBest(Map map) { if (map.isEmpty) return '---'; return namesMap[map.entries.reduce((a, b) => a.value > b.value ? a : b).key] ?? '---'; } - int getBestVal(Map map) { if (map.isEmpty) return 0; return map.values.reduce((a, b) => a > b ? a : b); } - return {'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap), 'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap), 'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)}; + + if (ptsMap.isEmpty) { + return { + 'pts_name': '---', + 'pts_val': 0, + 'ast_name': '---', + 'ast_val': 0, + 'rbs_name': '---', + 'rbs_val': 0, + }; + } + + String getBest(Map map) { + if (map.isEmpty) return '---'; + return namesMap[ + map.entries.reduce((a, b) => a.value > b.value ? a : b).key] ?? + '---'; + } + + int getBestVal(Map map) { + if (map.isEmpty) return 0; + return map.values.reduce((a, b) => a > b ? a : b); + } + + return { + 'pts_name': getBest(ptsMap), + 'pts_val': getBestVal(ptsMap), + 'ast_name': getBest(astMap), + 'ast_val': getBestVal(astMap), + 'rbs_name': getBest(rbsMap), + 'rbs_val': getBestVal(rbsMap), + }; } - Widget _buildStatCard({required BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) { + Widget _buildStatCard({ + required BuildContext context, + required String title, + required String playerName, + required String statValue, + required String statLabel, + required Color color, + bool isHighlighted = false, + }) { return Card( - elevation: 4, margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: AppTheme.warningAmber, width: 2) : BorderSide.none), + elevation: 4, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: isHighlighted + ? const BorderSide(color: AppTheme.warningAmber, width: 2) + : BorderSide.none, + ), child: Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [color.withOpacity(0.9), color], + ), + ), child: LayoutBuilder( builder: (context, constraints) { - final double ch = constraints.maxHeight; final double cw = constraints.maxWidth; + final double ch = constraints.maxHeight; + final double cw = constraints.maxWidth; return Padding( - padding: EdgeInsets.all(cw * 0.06), + padding: EdgeInsets.all(cw * 0.06), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis), + Text( + title.toUpperCase(), + style: TextStyle( + fontSize: ch * 0.06, + fontWeight: FontWeight.bold, + color: Colors.white70), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), SizedBox(height: ch * 0.011), - SizedBox(width: double.infinity, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)))), + SizedBox( + width: double.infinity, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text(playerName, + style: TextStyle( + fontSize: ch * 0.08, + fontWeight: FontWeight.bold, + color: Colors.white)), + ), + ), const Spacer(), - Center(child: FittedBox(fit: BoxFit.scaleDown, child: Text(statValue, style: TextStyle(fontSize: ch * 0.18, fontWeight: FontWeight.bold, color: Colors.white, height: 1.0)))), + Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + statValue, + style: TextStyle( + fontSize: ch * 0.18, + fontWeight: FontWeight.bold, + color: Colors.white, + height: 1.0), + ), + ), + ), SizedBox(height: ch * 0.015), - Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))), + Center( + child: Text(statLabel, + style: TextStyle( + fontSize: ch * 0.05, color: Colors.white70))), const Spacer(), - Container(width: double.infinity, padding: EdgeInsets.symmetric(vertical: ch * 0.035), decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)), child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold)))), + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: ch * 0.035), + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: BorderRadius.circular(ch * 0.03)), + child: Center( + child: Text('DETALHES', + style: TextStyle( + color: Colors.white, + fontSize: ch * 0.05, + fontWeight: FontWeight.bold))), + ), ], ), ); - } + }, ), ), ); } - Widget _buildGameHistoryCard({required BuildContext context, required String opponent, required String result, required int myScore, required int oppScore, required String date, required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp}) { - bool isWin = result == 'V'; bool isDraw = result == 'E'; - Color statusColor = isWin ? AppTheme.successGreen : (isDraw ? AppTheme.warningAmber : AppTheme.oppTeamRed); - final bgColor = Theme.of(context).cardTheme.color; final textColor = Theme.of(context).colorScheme.onSurface; + Widget _buildGameHistoryCard({ + required BuildContext context, + required String opponent, + required String result, + required int myScore, + required int oppScore, + required String date, + required String topPts, + required String topAst, + required String topRbs, + required String topDef, + required String mvp, + }) { + bool isWin = result == 'V'; + bool isDraw = result == 'E'; + Color statusColor = isWin + ? AppTheme.successGreen + : (isDraw ? AppTheme.warningAmber : AppTheme.oppTeamRed); + final bgColor = Theme.of(context).cardTheme.color; + final textColor = Theme.of(context).colorScheme.onSurface; return Container( - margin: EdgeInsets.only(bottom: 14 * context.sf), decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.withOpacity(0.1)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))]), + margin: EdgeInsets.only(bottom: 14 * context.sf), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.withOpacity(0.1)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 8, + offset: const Offset(0, 4)) + ], + ), child: Column( children: [ Padding( padding: EdgeInsets.all(14 * context.sf), child: Row( children: [ - Container(width: 36 * context.sf, height: 36 * context.sf, decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle), child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * context.sf)))), + Container( + width: 36 * context.sf, + height: 36 * context.sf, + decoration: BoxDecoration( + color: statusColor.withOpacity(0.15), + shape: BoxShape.circle), + child: Center( + child: Text(result, + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.bold, + fontSize: 16 * context.sf))), + ), SizedBox(width: 14 * context.sf), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(date, style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.w600)), + Text(date, + style: TextStyle( + fontSize: 11 * context.sf, + color: Colors.grey, + fontWeight: FontWeight.w600)), SizedBox(height: 6 * context.sf), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), maxLines: 1, overflow: TextOverflow.ellipsis)), - Padding(padding: EdgeInsets.symmetric(horizontal: 8 * context.sf), child: Container(padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), decoration: BoxDecoration(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), borderRadius: BorderRadius.circular(8)), child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * context.sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: textColor)))), - Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * context.sf, fontWeight: FontWeight.bold, color: textColor), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)), + Expanded( + child: Text( + _selectedTeamName == "Selecionar Equipa" + ? "Minha Equipa" + : _selectedTeamName, + style: TextStyle( + fontSize: 14 * context.sf, + fontWeight: FontWeight.bold, + color: textColor), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 8 * context.sf), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 8 * context.sf, + vertical: 4 * context.sf), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.05), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '$myScore - $oppScore', + style: TextStyle( + fontSize: 15 * context.sf, + fontWeight: FontWeight.w900, + letterSpacing: 1.5, + color: textColor), + ), + ), + ), + Expanded( + child: Text( + opponent, + style: TextStyle( + fontSize: 14 * context.sf, + fontWeight: FontWeight.bold, + color: textColor), + textAlign: TextAlign.right, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), ], ), ], @@ -465,30 +972,81 @@ class _HomeScreenState extends State { ], ), ), - Divider(height: 1, color: Colors.grey.withOpacity(0.1), thickness: 1.5), + Divider( + height: 1, + color: Colors.grey.withOpacity(0.1), + thickness: 1.5), Container( - width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16 * context.sf, vertical: 12 * context.sf), decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16))), + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 16 * context.sf, vertical: 12 * context.sf), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16)), + ), child: Column( children: [ - Row(children: [Expanded(child: _buildGridStatRow(context, Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, isMvp: true)), Expanded(child: _buildGridStatRow(context, Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef))]), + Row(children: [ + Expanded( + child: _buildGridStatRow(context, + Icons.workspace_premium, Colors.amber.shade700, + "MVP", mvp, isMvp: true)), + Expanded( + child: _buildGridStatRow(context, Icons.shield, + Colors.deepOrange.shade700, "Defesa", topDef)), + ]), SizedBox(height: 8 * context.sf), - Row(children: [Expanded(child: _buildGridStatRow(context, Icons.bolt, Colors.blue.shade700, "Pontos", topPts)), Expanded(child: _buildGridStatRow(context, Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs))]), + Row(children: [ + Expanded( + child: _buildGridStatRow(context, Icons.bolt, + Colors.blue.shade700, "Pontos", topPts)), + Expanded( + child: _buildGridStatRow(context, Icons.trending_up, + Colors.purple.shade700, "Rebotes", topRbs)), + ]), SizedBox(height: 8 * context.sf), - Row(children: [Expanded(child: _buildGridStatRow(context, Icons.star, Colors.green.shade700, "Assists", topAst)), const Expanded(child: SizedBox())]), + Row(children: [ + Expanded( + child: _buildGridStatRow(context, Icons.star, + Colors.green.shade700, "Assists", topAst)), + const Expanded(child: SizedBox()), + ]), ], ), - ) + ), ], ), ); } - Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, String label, String value, {bool isMvp = false}) { + Widget _buildGridStatRow(BuildContext context, IconData icon, Color color, + String label, String value, + {bool isMvp = false}) { return Row( children: [ - Icon(icon, size: 14 * context.sf, color: color), SizedBox(width: 4 * context.sf), - Text('$label: ', style: TextStyle(fontSize: 11 * context.sf, color: Colors.grey, fontWeight: FontWeight.bold)), - Expanded(child: Text(value, style: TextStyle(fontSize: 11 * context.sf, color: isMvp ? AppTheme.warningAmber : Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)), + Icon(icon, size: 14 * context.sf, color: color), + SizedBox(width: 4 * context.sf), + Text('$label: ', + style: TextStyle( + fontSize: 11 * context.sf, + color: Colors.grey, + fontWeight: FontWeight.bold)), + Expanded( + child: Text( + value, + style: TextStyle( + fontSize: 11 * context.sf, + color: isMvp + ? AppTheme.warningAmber + : Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), ], ); } diff --git a/lib/pages/settings_screen.dart b/lib/pages/settings_screen.dart index d659bed..e0908eb 100644 --- a/lib/pages/settings_screen.dart +++ b/lib/pages/settings_screen.dart @@ -33,13 +33,22 @@ class _SettingsScreenState extends State { _loadUserAvatar(); } + String _prefsKey(String key) { + final userId = supabase.auth.currentUser?.id ?? 'guest'; + return '${key}_$userId'; + } + Future _loadUserAvatar() async { final prefs = await SharedPreferences.getInstance(); - final savedUrl = prefs.getString('meu_avatar_guardado'); + final savedUrl = prefs.getString(_prefsKey('meu_avatar_guardado')); if (mounted) { setState(() { - if (savedUrl != null) _uploadedImageUrl = savedUrl; + if (savedUrl != null) { + _uploadedImageUrl = savedUrl; + } else { + _uploadedImageUrl = null; + } _isMemoryLoaded = true; }); } @@ -58,7 +67,7 @@ class _SettingsScreenState extends State { final urlDoSupabase = data['avatar_url']; if (urlDoSupabase != savedUrl) { - await prefs.setString('meu_avatar_guardado', urlDoSupabase); + await prefs.setString(_prefsKey('meu_avatar_guardado'), urlDoSupabase); setState(() { _uploadedImageUrl = urlDoSupabase; }); @@ -104,7 +113,7 @@ class _SettingsScreenState extends State { }); final prefs = await SharedPreferences.getInstance(); - await prefs.setString('meu_avatar_guardado', publicUrl); + await prefs.setString(_prefsKey('meu_avatar_guardado'), publicUrl); if (mounted) { setState(() { @@ -354,13 +363,16 @@ class _SettingsScreenState extends State { onPressed: () async { // 👇 AGORA LIMPA A EQUIPA E TUDO DA MEMÓRIA AO SAIR! final prefs = await SharedPreferences.getInstance(); - await prefs.remove('meu_avatar_guardado'); - await prefs.remove('last_team_id'); - await prefs.remove('last_team_name'); - await prefs.remove('last_team_logo'); - await prefs.remove('last_team_wins'); - await prefs.remove('last_team_losses'); - await prefs.remove('last_team_draws'); + final userId = supabase.auth.currentUser?.id; + if (userId != null) { + await prefs.remove(_prefsKey('meu_avatar_guardado')); + await prefs.remove(_prefsKey('last_team_id')); + await prefs.remove(_prefsKey('last_team_name')); + await prefs.remove(_prefsKey('last_team_logo')); + await prefs.remove(_prefsKey('last_team_wins')); + await prefs.remove(_prefsKey('last_team_losses')); + await prefs.remove(_prefsKey('last_team_draws')); + } await Supabase.instance.client.auth.signOut(); if (ctx.mounted) { diff --git a/lib/pages/status_page.dart b/lib/pages/status_page.dart index ffd4db7..00a111a 100644 --- a/lib/pages/status_page.dart +++ b/lib/pages/status_page.dart @@ -1,13 +1,22 @@ import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:playmaker/classe/theme.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:playmaker/classe/theme.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../controllers/team_controller.dart'; -import '../utils/size_extension.dart'; +import '../utils/size_extension.dart'; class StatusPage extends StatefulWidget { - const StatusPage({super.key}); + final String? initialTeamId; + final String initialTeamName; + final String? initialTeamLogo; + + const StatusPage({ + super.key, + this.initialTeamId, + this.initialTeamName = "Selecionar Equipa", + this.initialTeamLogo, + }); @override State createState() => _StatusPageState(); @@ -16,10 +25,10 @@ class StatusPage extends StatefulWidget { class _StatusPageState extends State { final TeamController _teamController = TeamController(); final _supabase = Supabase.instance.client; - - String? _selectedTeamId; - String _selectedTeamName = "Selecionar Equipa"; - String? _selectedTeamLogo; + + late String? _selectedTeamId; + late String _selectedTeamName; + late String? _selectedTeamLogo; String _sortColumn = 'pts'; bool _isAscending = false; @@ -27,34 +36,61 @@ class _StatusPageState extends State { @override void initState() { super.initState(); - _loadSelectedTeam(); + _selectedTeamId = widget.initialTeamId; + _selectedTeamName = widget.initialTeamName; + _selectedTeamLogo = widget.initialTeamLogo; + + // Se não vieram parâmetros da HomeScreen, tenta carregar do SharedPreferences + if (_selectedTeamId == null) { + _loadSelectedTeamFallback(); + } } - Future _loadSelectedTeam() async { - final prefs = await SharedPreferences.getInstance(); - final savedId = prefs.getString('last_team_id'); - - if (savedId != null && mounted) { + String _prefsKey(String key) { + final userId = _supabase.auth.currentUser?.id ?? 'guest'; + return '${key}_$userId'; + } + + @override + void didUpdateWidget(StatusPage oldWidget) { + super.didUpdateWidget(oldWidget); + // Quando a HomeScreen muda a equipa, a StatusPage atualiza automaticamente + if (widget.initialTeamId != oldWidget.initialTeamId) { setState(() { - _selectedTeamId = savedId; - _selectedTeamName = prefs.getString('last_team_name') ?? "Selecionar Equipa"; - _selectedTeamLogo = prefs.getString('last_team_logo'); + _selectedTeamId = widget.initialTeamId; + _selectedTeamName = widget.initialTeamName; + _selectedTeamLogo = widget.initialTeamLogo; }); } } - Future _saveSelectedTeam() async { + /// Fallback: só usado se a HomeScreen não passou nenhuma equipa ainda + Future _loadSelectedTeamFallback() async { + final prefs = await SharedPreferences.getInstance(); + final savedId = prefs.getString(_prefsKey('last_team_id')); + if (savedId != null && mounted) { + setState(() { + _selectedTeamId = savedId; + _selectedTeamName = prefs.getString(_prefsKey('last_team_name')) ?? "Selecionar Equipa"; + _selectedTeamLogo = prefs.getString(_prefsKey('last_team_logo')); + }); + } + } + + /// Guarda a equipa selecionada localmente (quando muda dentro da StatusPage) + Future _saveSelectedTeamLocally() async { final prefs = await SharedPreferences.getInstance(); if (_selectedTeamId != null) { - await prefs.setString('last_team_id', _selectedTeamId!); - await prefs.setString('last_team_name', _selectedTeamName); + await prefs.setString(_prefsKey('last_team_id'), _selectedTeamId!); + await prefs.setString(_prefsKey('last_team_name'), _selectedTeamName); if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) { - await prefs.setString('last_team_logo', _selectedTeamLogo!); + await prefs.setString(_prefsKey('last_team_logo'), _selectedTeamLogo!); } else { - await prefs.remove('last_team_logo'); + await prefs.remove(_prefsKey('last_team_logo')); } } + // Também guarda no Supabase final userId = _supabase.auth.currentUser?.id; if (userId != null && _selectedTeamId != null) { try { @@ -82,10 +118,12 @@ class _StatusPageState extends State { child: Container( padding: EdgeInsets.all(12 * context.sf), decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(15 * context.sf), + color: bgColor, + borderRadius: BorderRadius.circular(15 * context.sf), border: Border.all(color: Colors.grey.withOpacity(0.2)), - boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)] + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5) + ], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -98,14 +136,23 @@ class _StatusPageState extends State { 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), + 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.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), ], @@ -116,106 +163,204 @@ class _StatusPageState extends State { Expanded( child: _selectedTeamId == null - ? Center(child: Text("Seleciona uma equipa acima.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf))) - : StreamBuilder>>( - stream: _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!), - builder: (context, statsSnapshot) { - return StreamBuilder>>( - stream: _supabase.from('games').stream(primaryKey: ['id']).eq('my_team', _selectedTeamName), - builder: (context, gamesSnapshot) { - return StreamBuilder>>( - stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!), - builder: (context, membersSnapshot) { - if (statsSnapshot.connectionState == ConnectionState.waiting || gamesSnapshot.connectionState == ConnectionState.waiting || membersSnapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator(color: AppTheme.primaryRed)); - } + ? Center( + child: Text( + "Seleciona uma equipa acima.", + style: TextStyle( + color: Colors.grey, fontSize: 14 * context.sf), + ), + ) + : StreamBuilder>>( + stream: _supabase + .from('player_stats_with_names') + .stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!), + builder: (context, statsSnapshot) { + return StreamBuilder>>( + stream: _supabase + .from('games') + .stream(primaryKey: ['id']).eq('my_team', _selectedTeamName), + builder: (context, gamesSnapshot) { + return StreamBuilder>>( + stream: _supabase + .from('members') + .stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!), + builder: (context, membersSnapshot) { + if (statsSnapshot.connectionState == + ConnectionState.waiting || + gamesSnapshot.connectionState == + ConnectionState.waiting || + membersSnapshot.connectionState == + ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + color: AppTheme.primaryRed)); + } - final membersData = membersSnapshot.data ?? []; - if (membersData.isEmpty) return Center(child: Text("Esta equipa não tem jogadores registados.", style: TextStyle(color: Colors.grey, fontSize: 14 * context.sf))); + final membersData = membersSnapshot.data ?? []; + if (membersData.isEmpty) { + return Center( + child: Text( + "Esta equipa não tem jogadores registados.", + style: TextStyle( + color: Colors.grey, + fontSize: 14 * context.sf))); + } - final statsData = statsSnapshot.data ?? []; - final gamesData = gamesSnapshot.data ?? []; - final totalGamesPlayedByTeam = gamesData.where((g) => g['status'] == 'Terminado').length; + final statsData = statsSnapshot.data ?? []; + final gamesData = gamesSnapshot.data ?? []; + final totalGamesPlayedByTeam = gamesData + .where((g) => g['status'] == 'Terminado') + .length; - final List> playerTotals = _aggregateStats(statsData, gamesData, membersData); - final teamTotals = _calculateTeamTotals(playerTotals, totalGamesPlayedByTeam); + final List> playerTotals = + _aggregateStats(statsData, gamesData, membersData); + final teamTotals = _calculateTeamTotals( + playerTotals, totalGamesPlayedByTeam); - playerTotals.sort((a, b) { - var valA = a[_sortColumn] ?? 0; - var valB = b[_sortColumn] ?? 0; - return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA); - }); + playerTotals.sort((a, b) { + var valA = a[_sortColumn] ?? 0; + var valB = b[_sortColumn] ?? 0; + return _isAscending + ? valA.compareTo(valB) + : valB.compareTo(valA); + }); - return _buildStatsGrid(context, playerTotals, teamTotals, bgColor, textColor); - } - ); - } - ); - } - ), + return _buildStatsGrid( + context, playerTotals, teamTotals, bgColor, textColor); + }, + ); + }, + ); + }, + ), ), ], ); } - List> _aggregateStats(List stats, List games, List members) { + List> _aggregateStats( + List stats, List games, List members) { Map> aggregated = {}; + for (var member in members) { String name = member['name']?.toString() ?? "Desconhecido"; - String? imageUrl = member['image_url']?.toString(); - aggregated[name] = {'name': name, 'image_url': imageUrl, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0}; + String? imageUrl = member['image_url']?.toString(); + aggregated[name] = { + 'name': name, + 'image_url': imageUrl, + 'j': 0, + 'pts': 0, + 'ast': 0, + 'rbs': 0, + 'stl': 0, + 'blk': 0, + 'mvp': 0, + 'def': 0, + }; } + for (var row in stats) { String name = row['player_name']?.toString() ?? "Desconhecido"; - if (!aggregated.containsKey(name)) aggregated[name] = {'name': name, 'image_url': null, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0}; - - aggregated[name]!['j'] += 1; + if (!aggregated.containsKey(name)) { + aggregated[name] = { + 'name': name, + 'image_url': null, + 'j': 0, + 'pts': 0, + 'ast': 0, + 'rbs': 0, + 'stl': 0, + 'blk': 0, + 'mvp': 0, + 'def': 0, + }; + } + aggregated[name]!['j'] += 1; aggregated[name]!['pts'] += (row['pts'] ?? 0); aggregated[name]!['ast'] += (row['ast'] ?? 0); aggregated[name]!['rbs'] += (row['rbs'] ?? 0); aggregated[name]!['stl'] += (row['stl'] ?? 0); aggregated[name]!['blk'] += (row['blk'] ?? 0); } + for (var game in games) { String? mvp = game['mvp_name']; String? defRaw = game['top_def_name']; - if (mvp != null && aggregated.containsKey(mvp)) aggregated[mvp]!['mvp'] += 1; + if (mvp != null && aggregated.containsKey(mvp)) { + aggregated[mvp]!['mvp'] += 1; + } if (defRaw != null) { - String defName = defRaw.split(' (')[0].trim(); - if (aggregated.containsKey(defName)) aggregated[defName]!['def'] += 1; + String defName = defRaw.split(' (')[0].trim(); + if (aggregated.containsKey(defName)) { + aggregated[defName]!['def'] += 1; + } } } + return aggregated.values.toList(); } - Map _calculateTeamTotals(List> players, int teamGames) { - int tPts = 0, tAst = 0, tRbs = 0, tStl = 0, tBlk = 0, tMvp = 0, tDef = 0; + Map _calculateTeamTotals( + List> players, int teamGames) { + int tPts = 0, + tAst = 0, + tRbs = 0, + tStl = 0, + tBlk = 0, + tMvp = 0, + tDef = 0; for (var p in players) { - tPts += (p['pts'] as int); tAst += (p['ast'] as int); tRbs += (p['rbs'] as int); tStl += (p['stl'] as int); tBlk += (p['blk'] as int); tMvp += (p['mvp'] as int); tDef += (p['def'] as int); + tPts += (p['pts'] as int); + tAst += (p['ast'] as int); + tRbs += (p['rbs'] as int); + tStl += (p['stl'] as int); + tBlk += (p['blk'] as int); + tMvp += (p['mvp'] as int); + tDef += (p['def'] as int); } - return {'name': 'TOTAL EQUIPA', 'image_url': null, 'j': teamGames, 'pts': tPts, 'ast': tAst, 'rbs': tRbs, 'stl': tStl, 'blk': tBlk, 'mvp': tMvp, 'def': tDef}; + return { + 'name': 'TOTAL EQUIPA', + 'image_url': null, + 'j': teamGames, + 'pts': tPts, + 'ast': tAst, + 'rbs': tRbs, + 'stl': tStl, + 'blk': tBlk, + 'mvp': tMvp, + 'def': tDef, + }; } - Widget _buildStatsGrid(BuildContext context, List> players, Map teamTotals, Color bgColor, Color textColor) { + Widget _buildStatsGrid( + BuildContext context, + List> players, + Map teamTotals, + Color bgColor, + Color textColor) { return Container( - color: Colors.transparent, // 👇 VOLTOU A ESTAR TRANSPARENTE COMO TINHAS ANTES! + color: Colors.transparent, width: double.infinity, child: SingleChildScrollView( scrollDirection: Axis.vertical, physics: const BouncingScrollPhysics(), child: SingleChildScrollView( scrollDirection: Axis.horizontal, - physics: const ClampingScrollPhysics(), // Mantém-se o Clamping para não puxar mais do que o ecrã + physics: const ClampingScrollPhysics(), child: ConstrainedBox( - constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width), + constraints: + BoxConstraints(minWidth: MediaQuery.of(context).size.width), child: DataTable( - columnSpacing: 20 * context.sf, - horizontalMargin: 16 * context.sf, - headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface), + columnSpacing: 20 * context.sf, + horizontalMargin: 16 * context.sf, + headingRowColor: WidgetStateProperty.all( + Theme.of(context).colorScheme.surface), dataRowMaxHeight: 60 * context.sf, dataRowMinHeight: 60 * context.sf, columns: [ - DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))), + DataColumn( + label: Text('JOGADOR', + style: TextStyle(color: textColor))), _buildSortableColumn(context, 'J', 'j', textColor), _buildSortableColumn(context, 'PTS', 'pts', textColor), _buildSortableColumn(context, 'AST', 'ast', textColor), @@ -227,53 +372,83 @@ class _StatusPageState extends State { ], rows: [ ...players.map((player) => DataRow(cells: [ - DataCell( - Row( - children: [ - ClipOval( - child: Container( - width: 30 * context.sf, - height: 30 * context.sf, - color: Colors.grey.withOpacity(0.2), - child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty) - ? CachedNetworkImage( - imageUrl: player['image_url'], - fit: BoxFit.cover, - fadeInDuration: Duration.zero, - placeholder: (context, url) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey), - errorWidget: (context, url, error) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey), - ) - : Icon(Icons.person, size: 18 * context.sf, color: Colors.grey), + DataCell( + Row(children: [ + ClipOval( + child: Container( + width: 30 * context.sf, + height: 30 * context.sf, + color: Colors.grey.withOpacity(0.2), + child: (player['image_url'] != null && + player['image_url'] + .toString() + .isNotEmpty) + ? CachedNetworkImage( + imageUrl: player['image_url'], + fit: BoxFit.cover, + fadeInDuration: Duration.zero, + placeholder: (context, url) => Icon( + Icons.person, + size: 18 * context.sf, + color: Colors.grey), + errorWidget: (context, url, error) => + Icon(Icons.person, + size: 18 * context.sf, + color: Colors.grey), + ) + : Icon(Icons.person, + size: 18 * context.sf, + color: Colors.grey), + ), ), - ), - SizedBox(width: 10 * context.sf), - Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor)) - ] - ) - ), - DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))), - _buildStatCell(context, player['pts'], textColor, isHighlight: true), - _buildStatCell(context, player['ast'], textColor), - _buildStatCell(context, player['rbs'], textColor), - _buildStatCell(context, player['stl'], textColor), - _buildStatCell(context, player['blk'], textColor), - _buildStatCell(context, player['def'], textColor, isBlue: true), - _buildStatCell(context, player['mvp'], textColor, isGold: true), - ])), + SizedBox(width: 10 * context.sf), + Text(player['name'], + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13 * context.sf, + color: textColor)), + ]), + ), + DataCell(Center( + child: Text(player['j'].toString(), + style: TextStyle(color: textColor)))), + _buildStatCell(context, player['pts'], textColor, + isHighlight: true), + _buildStatCell(context, player['ast'], textColor), + _buildStatCell(context, player['rbs'], textColor), + _buildStatCell(context, player['stl'], textColor), + _buildStatCell(context, player['blk'], textColor), + _buildStatCell(context, player['def'], textColor, + isBlue: true), + _buildStatCell(context, player['mvp'], textColor, + isGold: true), + ])), DataRow( - color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)), + color: WidgetStateProperty.all( + Theme.of(context).colorScheme.surface.withOpacity(0.5)), cells: [ - DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: textColor, fontSize: 12 * context.sf))), - DataCell(Center(child: Text(teamTotals['j'].toString(), style: TextStyle(fontWeight: FontWeight.bold, color: textColor)))), - _buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true), + DataCell(Text('TOTAL EQUIPA', + style: TextStyle( + fontWeight: FontWeight.w900, + color: textColor, + fontSize: 12 * context.sf))), + DataCell(Center( + child: Text(teamTotals['j'].toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + color: textColor)))), + _buildStatCell(context, teamTotals['pts'], textColor, + isHighlight: true), _buildStatCell(context, teamTotals['ast'], textColor), _buildStatCell(context, teamTotals['rbs'], textColor), _buildStatCell(context, teamTotals['stl'], textColor), _buildStatCell(context, teamTotals['blk'], textColor), - _buildStatCell(context, teamTotals['def'], textColor, isBlue: true), - _buildStatCell(context, teamTotals['mvp'], textColor, isGold: true), - ] - ) + _buildStatCell(context, teamTotals['def'], textColor, + isBlue: true), + _buildStatCell(context, teamTotals['mvp'], textColor, + isGold: true), + ], + ), ], ), ), @@ -282,70 +457,123 @@ class _StatusPageState extends State { ); } - DataColumn _buildSortableColumn(BuildContext context, String title, String sortKey, Color textColor) { - return DataColumn(label: InkWell( - onTap: () => setState(() { - if (_sortColumn == sortKey) _isAscending = !_isAscending; - else { _sortColumn = sortKey; _isAscending = false; } - }), - child: Row(children: [ - Text(title, style: TextStyle(fontSize: 12 * context.sf, fontWeight: FontWeight.bold, color: textColor)), - if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * context.sf, color: AppTheme.primaryRed), - ]), - )); + DataColumn _buildSortableColumn( + BuildContext context, String title, String sortKey, Color textColor) { + return DataColumn( + label: InkWell( + onTap: () => setState(() { + if (_sortColumn == sortKey) { + _isAscending = !_isAscending; + } else { + _sortColumn = sortKey; + _isAscending = false; + } + }), + child: Row(children: [ + Text(title, + style: TextStyle( + fontSize: 12 * context.sf, + fontWeight: FontWeight.bold, + color: textColor)), + if (_sortColumn == sortKey) + Icon( + _isAscending + ? Icons.arrow_drop_up + : Icons.arrow_drop_down, + size: 18 * context.sf, + color: AppTheme.primaryRed), + ]), + ), + ); } - DataCell _buildStatCell(BuildContext context, int value, Color textColor, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) { - return DataCell(Center(child: Container( - padding: EdgeInsets.symmetric(horizontal: 8 * context.sf, vertical: 4 * context.sf), - decoration: BoxDecoration(color: isGold && value > 0 ? Colors.amber.withOpacity(0.2) : (isBlue && value > 0 ? Colors.blue.withOpacity(0.1) : Colors.transparent), borderRadius: BorderRadius.circular(6)), - child: Text(value == 0 ? "-" : value.toString(), style: TextStyle( - fontWeight: (isHighlight || isGold || isBlue) ? FontWeight.w900 : FontWeight.w600, - fontSize: 14 * context.sf, color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? AppTheme.successGreen : textColor)) - )), - ))); + DataCell _buildStatCell(BuildContext context, int value, Color textColor, + {bool isHighlight = false, bool isGold = false, bool isBlue = false}) { + return DataCell(Center( + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 8 * context.sf, vertical: 4 * context.sf), + decoration: BoxDecoration( + color: isGold && value > 0 + ? Colors.amber.withOpacity(0.2) + : (isBlue && value > 0 + ? Colors.blue.withOpacity(0.1) + : Colors.transparent), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + value == 0 ? "-" : value.toString(), + style: TextStyle( + fontWeight: (isHighlight || isGold || isBlue) + ? FontWeight.w900 + : FontWeight.w600, + fontSize: 14 * context.sf, + color: isGold && value > 0 + ? Colors.orange.shade900 + : (isBlue && value > 0 + ? Colors.blue.shade800 + : (isHighlight ? AppTheme.successGreen : textColor)), + ), + ), + ), + )); } void _showTeamSelector(BuildContext context) { - showModalBottomSheet(context: context, backgroundColor: Theme.of(context).colorScheme.surface, builder: (context) => StreamBuilder>>( - stream: _teamController.teamsStream, - builder: (context, snapshot) { - final teams = snapshot.data ?? []; - return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) { - final team = teams[i]; - final logoUrl = team['image_url']; - - return ListTile( - leading: ClipOval( - child: Container( - width: 36 * context.sf, - height: 36 * context.sf, - color: AppTheme.primaryRed.withOpacity(0.1), - child: (logoUrl != null && logoUrl.isNotEmpty) - ? CachedNetworkImage( - imageUrl: logoUrl, - fit: BoxFit.cover, - placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), - errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), - ) - : Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf), - ), - ), - title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), - onTap: () async { - setState(() { - _selectedTeamId = team['id'].toString(); - _selectedTeamName = team['name']; - _selectedTeamLogo = logoUrl; - }); - - await _saveSelectedTeam(); - - if (context.mounted) Navigator.pop(context); + showModalBottomSheet( + context: context, + backgroundColor: Theme.of(context).colorScheme.surface, + builder: (context) => StreamBuilder>>( + stream: _teamController.teamsStream, + builder: (context, snapshot) { + final teams = snapshot.data ?? []; + return ListView.builder( + itemCount: teams.length, + itemBuilder: (context, i) { + final team = teams[i]; + final logoUrl = team['image_url']; + + return ListTile( + leading: ClipOval( + child: Container( + width: 36 * context.sf, + height: 36 * context.sf, + color: AppTheme.primaryRed.withOpacity(0.1), + child: (logoUrl != null && logoUrl.isNotEmpty) + ? CachedNetworkImage( + imageUrl: logoUrl, + fit: BoxFit.cover, + placeholder: (context, url) => Icon(Icons.shield, + color: AppTheme.primaryRed, + size: 20 * context.sf), + errorWidget: (context, url, error) => Icon( + Icons.shield, + color: AppTheme.primaryRed, + size: 20 * context.sf), + ) + : Icon(Icons.shield, + color: AppTheme.primaryRed, size: 20 * context.sf), + ), + ), + title: Text(team['name'], + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface)), + onTap: () async { + setState(() { + _selectedTeamId = team['id'].toString(); + _selectedTeamName = team['name']; + _selectedTeamLogo = logoUrl; + }); + + await _saveSelectedTeamLocally(); + + if (context.mounted) Navigator.pop(context); + }, + ); }, ); - }); - }, - )); + }, + ), + ); } } \ No newline at end of file diff --git a/lib/widgets/share_game_dialog.dart b/lib/widgets/share_game_dialog.dart new file mode 100644 index 0000000..cf4a92a --- /dev/null +++ b/lib/widgets/share_game_dialog.dart @@ -0,0 +1,239 @@ +import 'package:flutter/material.dart'; +import 'package:playmaker/classe/theme.dart'; +import 'package:playmaker/controllers/game_sharing_controller.dart'; + +class ShareGameDialog extends StatefulWidget { + final String gameId; + final GameSharingController controller; + final String? activeSessionId; + final String? activeShareCode; + + const ShareGameDialog({ + super.key, + required this.gameId, + required this.controller, + this.activeSessionId, + this.activeShareCode, + }); + + @override + State createState() => _ShareGameDialogState(); +} + +class _ShareGameDialogState extends State { + String? _shareCode; + bool _isLoading = false; + String? _error; + + @override + void initState() { + super.initState(); + _shareCode = widget.activeShareCode; + } + + Future _createSession() async { + setState(() { + _error = null; + _isLoading = true; + }); + + final code = await widget.controller.createShareSession(widget.gameId); + if (!mounted) return; + + setState(() { + _isLoading = false; + if (code == null) { + _error = 'Erro ao criar sessão. Tenta novamente.'; + } else { + _shareCode = code; + } + }); + + if (_shareCode != null && widget.activeSessionId == null) { + final session = await widget.controller.getActiveSessionForGame( + widget.gameId, + ); + if (session != null && mounted) { + Navigator.of( + context, + ).pop({'session_id': session['id'], 'share_code': _shareCode}); + } + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text( + 'Partilhar Jogo', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_shareCode != null) ...[ + Text( + 'Código de partilha:', + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox(height: 10), + SelectableText( + _shareCode!, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: AppTheme.primaryRed, + ), + ), + const SizedBox(height: 10), + Text( + 'Envie este código ao seu parceiro e peça que ele entre no jogo.', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), + ), + ), + ] else ...[ + Text( + 'Crie um código e partilhe o jogo com outro utilizador.', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), + ), + ), + ], + if (_error != null) ...[ + const SizedBox(height: 10), + Text(_error!, style: const TextStyle(color: Colors.red)), + ], + ], + ), + actions: [ + TextButton( + onPressed: _isLoading ? null : () => Navigator.of(context).pop(), + child: const Text('Fechar'), + ), + if (_shareCode == null) + TextButton( + onPressed: _isLoading ? null : _createSession, + child: _isLoading + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Criar código'), + ) + else + TextButton( + onPressed: () { + Navigator.of(context).pop({ + 'session_id': widget.activeSessionId, + 'share_code': _shareCode, + }); + }, + child: const Text('OK'), + ), + ], + ); + } +} + +class JoinGameDialog extends StatefulWidget { + final GameSharingController controller; + + const JoinGameDialog({super.key, required this.controller}); + + @override + State createState() => _JoinGameDialogState(); +} + +class _JoinGameDialogState extends State { + final TextEditingController _codeController = TextEditingController(); + bool _isLoading = false; + String? _error; + + Future _joinSession() async { + setState(() { + _isLoading = true; + _error = null; + }); + + final result = await widget.controller.joinGameByCode( + _codeController.text.trim(), + ); + if (!mounted) return; + + setState(() { + _isLoading = false; + }); + + if (result == null) { + setState(() { + _error = 'Código inválido ou sessão não disponível.'; + }); + return; + } + + Navigator.of(context).pop(result); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text( + 'Entrar em Jogo Partilhado', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Digite o código enviado pelo outro utilizador.', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _codeController, + textCapitalization: TextCapitalization.characters, + decoration: InputDecoration( + labelText: 'Código', + border: const OutlineInputBorder(), + hintText: 'ABC123', + ), + ), + if (_error != null) ...[ + const SizedBox(height: 12), + Text(_error!, style: const TextStyle(color: Colors.red)), + ], + ], + ), + actions: [ + TextButton( + onPressed: _isLoading ? null : () => Navigator.of(context).pop(), + child: const Text('Cancelar'), + ), + TextButton( + onPressed: _isLoading ? null : _joinSession, + child: _isLoading + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Entrar'), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index f3e2aba..9c4f8e5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -516,18 +516,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -953,10 +953,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" typed_data: dependency: transitive description: