import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:provider/provider.dart'; import 'package:farolero/l10n/generated/app_localizations.dart'; import '../modelos/jugador.dart'; import '../servicios/servicio_nearby.dart'; import '../servicios/servicio_permisos.dart'; import '../tema/tema_app.dart'; import 'pantalla_palabra_cliente.dart'; import 'pantalla_debate_cliente.dart'; import 'pantalla_votacion_cliente.dart'; /// Pantalla para unirse a una partida multidispositivo. /// Flujo: nombre → discovery automático (lista de salas) → fallback QR class PantallaUnirse extends StatefulWidget { const PantallaUnirse({super.key}); @override State createState() => _PantallaUnirseState(); } class _PantallaUnirseState extends State { final _nombreController = TextEditingController(); final _formKey = GlobalKey(); // Estados de la pantalla bool _buscando = false; bool _escaneandoQR = false; bool _conectando = false; String? _error; String? _salaSeleccionada; // Estado del juego recibido del host String? _palabraRecibida; bool _esImpostor = false; String? _pistaCategoria; final List _jugadores = []; @override void initState() { super.initState(); // Registrar listener ANTES del primer build WidgetsBinding.instance.addPostFrameCallback((_) { _registrarListenerPartida(); }); } void _registrarListenerPartida() { final nearby = context.read(); nearby.onMensaje((endpointId, mensaje) { if (mensaje.tipo == TipoMensaje.partidaInicio) { // El host ha iniciado la partida — nos ha enviado nuestra palabra setState(() { _palabraRecibida = mensaje.datos['palabra'] as String?; _esImpostor = mensaje.datos['esImpostor'] as bool? ?? false; _pistaCategoria = mensaje.datos['categoria'] as String?; }); // Navegar a pantalla de palabra del cliente if (mounted && _palabraRecibida != null) { _navegarAPalabra(); } } else if (mensaje.tipo == TipoMensaje.fase) { // El host cambia de fase — navegar a la pantalla correspondiente final fase = mensaje.datos['fase'] as String?; if (mounted && fase != null) { _navegarSegunFase(fase); } } }); } void _navegarAPalabra() { Navigator.of(context).push( MaterialPageRoute( builder: (_) => PantallaPalabraCliente( palabra: _palabraRecibida ?? '', esImpostor: _esImpostor, pistaCategoria: _pistaCategoria, onVisto: () { // Enviar "listo" al host y volver a la espera final nearby = context.read(); if (nearby.hostEndpointId != null) { nearby.enviarMensaje( nearby.hostEndpointId!, MensajeP2P(tipo: TipoMensaje.listo, datos: {}), ); } Navigator.of(context).pop(); }, ), ), ); } void _navegarSegunFase(String fase) { switch (fase) { case 'debate': Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (_) => PantallaDebateCliente( tiempoDebateSegundos: null, onSolicitarVotacion: () { final nearby = context.read(); if (nearby.hostEndpointId != null) { nearby.enviarMensaje( nearby.hostEndpointId!, MensajeP2P(tipo: TipoMensaje.ping, datos: {'solicitoVotacion': true}), ); } }, ), ), ); break; case 'votacion': Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (_) => PantallaVotacionCliente( jugadores: _jugadores, onVoto: (votoporId) { final nearby = context.read(); if (nearby.hostEndpointId != null) { nearby.enviarMensaje( nearby.hostEndpointId!, MensajeP2P(tipo: TipoMensaje.voto, datos: {'votoporId': votoporId}), ); } Navigator.of(context).pop(); }, ), ), ); break; } } @override void dispose() { _nombreController.dispose(); super.dispose(); } /// Paso 1: validar nombre, pedir permisos e iniciar discovery Future _iniciarBusqueda() async { if (!_formKey.currentState!.validate()) return; // Solicitar permisos automáticamente final permisosOk = await ServicioPermisos.solicitarPermisosNearby(context); if (!permisosOk) { setState(() { _error = 'Se necesitan permisos de Bluetooth y ubicación para buscar partidas.'; }); return; } if (!mounted) return; final nearby = context.read(); final ok = await nearby.buscarHosts(_nombreController.text.trim()); if (ok) { setState(() { _buscando = true; _error = null; }); } else { setState(() { _error = 'No se pudo iniciar la búsqueda. Verifica Bluetooth y ubicación.'; }); } } /// Conectar a un host de la lista Future _conectarAHost(String endpointId, String nombreHost) async { setState(() { _conectando = true; _salaSeleccionada = nombreHost; }); final nearby = context.read(); // Parar discovery antes de conectar await nearby.pararBusqueda(); final ok = await nearby.conectarAHost(endpointId, _nombreController.text.trim()); if (!ok && mounted) { setState(() { _conectando = false; _error = 'No se pudo conectar a $nombreHost'; }); // Reiniciar búsqueda _iniciarBusqueda(); } } /// Fallback: escanear QR void _abrirEscaner() { setState(() { _escaneandoQR = true; _error = null; }); } Future _onQRDetectado(BarcodeCapture capture) async { if (_conectando) return; for (final barcode in capture.barcodes) { final valor = barcode.rawValue; if (valor == null) continue; final datos = ServicioNearby.parsearQR(valor); if (datos != null) { setState(() { _escaneandoQR = false; _conectando = true; _salaSeleccionada = datos['host'] as String? ?? datos['sala'] as String? ?? 'Sala'; }); // Iniciar búsqueda para que Nearby encuentre al host final nearby = context.read(); if (!nearby.buscando) { await nearby.buscarHosts(_nombreController.text.trim()); } return; } } } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final nearby = context.watch(); // Si estamos conectados → pantalla de espera if (nearby.conectado && !nearby.esHost) { return _buildPantallaEspera(context, l10n); } // Si escaneando QR if (_escaneandoQR) { return _buildEscaner(context, l10n); } // Si buscando hosts o conectando if (_buscando || _conectando) { return _buildDiscovery(context, l10n, nearby); } // Formulario nombre return _buildFormularioNombre(context, l10n); } // ==================== PASO 1: NOMBRE ==================== Widget _buildFormularioNombre(BuildContext context, AppLocalizations l10n) { return Scaffold( appBar: AppBar( title: Text(l10n.joinGameTitle), ), body: Padding( padding: const EdgeInsets.all(32), child: Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('📱', style: TextStyle(fontSize: 64)), const SizedBox(height: 24), Text( l10n.joinGameTitle, style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 8), Text( l10n.enterNameToSearch, style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), const SizedBox(height: 32), TextFormField( controller: _nombreController, decoration: InputDecoration( labelText: l10n.yourName, prefixIcon: const Icon(Icons.person), ), validator: (v) { if (v == null || v.trim().isEmpty) return l10n.nameRequired; return null; }, textCapitalization: TextCapitalization.words, onFieldSubmitted: (_) => _iniciarBusqueda(), ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _iniciarBusqueda, icon: const Icon(Icons.search), label: Text(l10n.searchGames), ), ), if (_error != null) ...[ const SizedBox(height: 16), _buildError(_error!), ], ], ), ), ), ); } // ==================== PASO 2: DISCOVERY ==================== Widget _buildDiscovery(BuildContext context, AppLocalizations l10n, ServicioNearby nearby) { final hosts = nearby.hostsEncontrados; return Scaffold( appBar: AppBar( title: Text(l10n.joinGameTitle), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () async { await nearby.pararBusqueda(); setState(() { _buscando = false; _conectando = false; }); }, ), ), body: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ // Estado if (_conectando) ...[ const CircularProgressIndicator(color: TemaApp.colorAcento), const SizedBox(height: 12), Text( '${l10n.connectingTo} ${_salaSeleccionada ?? ""}...', style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 24), ] else ...[ // Buscando Row( children: [ const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: TemaApp.colorNaranja, ), ), const SizedBox(width: 12), Text( l10n.searchingGames, style: Theme.of(context).textTheme.titleMedium, ), ], ), const SizedBox(height: 24), ], // Lista de hosts encontrados Expanded( child: hosts.isEmpty && !_conectando ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('📡', style: TextStyle(fontSize: 48)), const SizedBox(height: 16), Text( l10n.noGamesFound, style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( l10n.noGamesFoundHint, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.grey, ), textAlign: TextAlign.center, ), ], ), ) : ListView.builder( itemCount: hosts.length, itemBuilder: (context, index) { final entry = hosts.entries.elementAt(index); return _buildHostTile(entry.key, entry.value); }, ), ), if (_error != null) ...[ _buildError(_error!), const SizedBox(height: 12), ], // Fallback: escanear QR if (!_conectando) ...[ const Divider(), const SizedBox(height: 8), Text( l10n.orScanQR, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.grey, ), ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: _abrirEscaner, icon: const Icon(Icons.qr_code_scanner), label: Text(l10n.scanQR), ), ), ], ], ), ), ); } Widget _buildHostTile(String endpointId, String nombre) { return Container( margin: const EdgeInsets.only(bottom: 8), child: Material( color: TemaApp.colorTarjeta, borderRadius: BorderRadius.circular(12), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: _conectando ? null : () => _conectarAHost(endpointId, nombre), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: Row( children: [ const Text('🎭', style: TextStyle(fontSize: 28)), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( nombre, style: Theme.of(context).textTheme.titleMedium, ), Text( 'Toca para unirte', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.grey, ), ), ], ), ), const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey), ], ), ), ), ), ); } // ==================== ESCÁNER QR ==================== Widget _buildEscaner(BuildContext context, AppLocalizations l10n) { return Scaffold( appBar: AppBar( title: Text(l10n.scanQR), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => setState(() => _escaneandoQR = false), ), ), body: Stack( children: [ MobileScanner(onDetect: _onQRDetectado), Positioned( bottom: 0, left: 0, right: 0, child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black.withValues(alpha: 0.8), ], ), ), child: Text( l10n.scanHostQR, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Colors.white, ), textAlign: TextAlign.center, ), ), ), ], ), ); } // ==================== ESPERA ==================== Widget _buildPantallaEspera(BuildContext context, AppLocalizations l10n) { return Scaffold( appBar: AppBar( title: Text(_salaSeleccionada ?? l10n.joinGameTitle), leading: IconButton( icon: const Icon(Icons.close), onPressed: () async { final nearby = context.read(); await nearby.desconectar(); if (context.mounted) { setState(() { _buscando = false; _conectando = false; }); } }, ), ), body: Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('✅', style: TextStyle(fontSize: 64)), const SizedBox(height: 24), Text( l10n.connectedWaiting, style: Theme.of(context).textTheme.headlineMedium, textAlign: TextAlign.center, ), const SizedBox(height: 12), Text( '${l10n.yourName}: ${_nombreController.text}', style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 32), const CircularProgressIndicator(color: TemaApp.colorNaranja), const SizedBox(height: 16), Text( l10n.waitingForHost, style: Theme.of(context).textTheme.bodyMedium, ), ], ), ), ), ); } // ==================== HELPERS ==================== Widget _buildError(String msg) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: TemaApp.colorAcento.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Text( msg, style: const TextStyle(color: TemaApp.colorAcento), textAlign: TextAlign.center, ), ); } }