diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 601edd4..2c2fc56 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -249,5 +249,11 @@ "scanQR": "Scan QR", "scanHostQR": "Point at the host's QR code", "connectedWaiting": "Connected!", - "waitingForHost": "Waiting for the host to start the game..." + "waitingForHost": "Waiting for the host to start the game...", + "enterNameToSearch": "Enter your name to search for nearby games", + "searchGames": "Search games", + "searchingGames": "Searching for nearby games...", + "noGamesFound": "No games found", + "noGamesFoundHint": "Make sure the host has the room open and you are nearby", + "orScanQR": "Not showing up? Scan the host's QR code" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4f2195a..922c05d 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -249,5 +249,11 @@ "scanQR": "Escanear QR", "scanHostQR": "Apunta al QR del host", "connectedWaiting": "¡Conectado!", - "waitingForHost": "Esperando a que el host inicie la partida..." + "waitingForHost": "Esperando a que el host inicie la partida...", + "enterNameToSearch": "Escribe tu nombre para buscar partidas cercanas", + "searchGames": "Buscar partidas", + "searchingGames": "Buscando partidas cercanas...", + "noGamesFound": "No se encontraron partidas", + "noGamesFoundHint": "Asegúrate de que el host tiene la sala abierta y estáis cerca", + "orScanQR": "¿No aparece? Escanea el QR del host" } \ No newline at end of file diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index f684d96..2352270 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -1070,6 +1070,42 @@ abstract class AppLocalizations { /// In es, this message translates to: /// **'Esperando a que el host inicie la partida...'** String get waitingForHost; + + /// No description provided for @enterNameToSearch. + /// + /// In es, this message translates to: + /// **'Escribe tu nombre para buscar partidas cercanas'** + String get enterNameToSearch; + + /// No description provided for @searchGames. + /// + /// In es, this message translates to: + /// **'Buscar partidas'** + String get searchGames; + + /// No description provided for @searchingGames. + /// + /// In es, this message translates to: + /// **'Buscando partidas cercanas...'** + String get searchingGames; + + /// No description provided for @noGamesFound. + /// + /// In es, this message translates to: + /// **'No se encontraron partidas'** + String get noGamesFound; + + /// No description provided for @noGamesFoundHint. + /// + /// In es, this message translates to: + /// **'Asegúrate de que el host tiene la sala abierta y estáis cerca'** + String get noGamesFoundHint; + + /// No description provided for @orScanQR. + /// + /// In es, this message translates to: + /// **'¿No aparece? Escanea el QR del host'** + String get orScanQR; } class _AppLocalizationsDelegate diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index dc41020..d7b2a38 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -511,4 +511,24 @@ class AppLocalizationsAr extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_ca.dart b/lib/l10n/generated/app_localizations_ca.dart index a6da1e1..cbd3459 100644 --- a/lib/l10n/generated/app_localizations_ca.dart +++ b/lib/l10n/generated/app_localizations_ca.dart @@ -514,4 +514,24 @@ class AppLocalizationsCa extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 8b9e2dd..b5c24d5 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -517,4 +517,24 @@ class AppLocalizationsDe extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index a679eaf..2aaeee7 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -512,4 +512,23 @@ class AppLocalizationsEn extends AppLocalizations { @override String get waitingForHost => 'Waiting for the host to start the game...'; + + @override + String get enterNameToSearch => 'Enter your name to search for nearby games'; + + @override + String get searchGames => 'Search games'; + + @override + String get searchingGames => 'Searching for nearby games...'; + + @override + String get noGamesFound => 'No games found'; + + @override + String get noGamesFoundHint => + 'Make sure the host has the room open and you are nearby'; + + @override + String get orScanQR => 'Not showing up? Scan the host\'s QR code'; } diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index e467f87..4474ccd 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -513,4 +513,24 @@ class AppLocalizationsEs extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_eu.dart b/lib/l10n/generated/app_localizations_eu.dart index 0bbdd53..cf087cc 100644 --- a/lib/l10n/generated/app_localizations_eu.dart +++ b/lib/l10n/generated/app_localizations_eu.dart @@ -516,4 +516,24 @@ class AppLocalizationsEu extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index a57b250..49bf30a 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -514,4 +514,24 @@ class AppLocalizationsFr extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index db8c8a1..6e73ab8 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -513,4 +513,24 @@ class AppLocalizationsHi extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index 5d54513..a94844d 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -514,4 +514,24 @@ class AppLocalizationsIt extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index c6d030f..45040c1 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -511,4 +511,24 @@ class AppLocalizationsJa extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index f8e9175..47f58fc 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -511,4 +511,24 @@ class AppLocalizationsKo extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_nl.dart b/lib/l10n/generated/app_localizations_nl.dart index 32f7fde..e7f1c31 100644 --- a/lib/l10n/generated/app_localizations_nl.dart +++ b/lib/l10n/generated/app_localizations_nl.dart @@ -514,4 +514,24 @@ class AppLocalizationsNl extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart index f03c352..1cf3627 100644 --- a/lib/l10n/generated/app_localizations_pl.dart +++ b/lib/l10n/generated/app_localizations_pl.dart @@ -514,4 +514,24 @@ class AppLocalizationsPl extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 77bc5d4..452f56f 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -515,4 +515,24 @@ class AppLocalizationsPt extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index aec4de1..854d706 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -514,4 +514,24 @@ class AppLocalizationsRu extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_tr.dart b/lib/l10n/generated/app_localizations_tr.dart index 3b45621..9444193 100644 --- a/lib/l10n/generated/app_localizations_tr.dart +++ b/lib/l10n/generated/app_localizations_tr.dart @@ -513,4 +513,24 @@ class AppLocalizationsTr extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index 3413f19..b14294e 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -510,6 +510,26 @@ class AppLocalizationsZh extends AppLocalizations { @override String get waitingForHost => 'Esperando a que el host inicie la partida...'; + + @override + String get enterNameToSearch => + 'Escribe tu nombre para buscar partidas cercanas'; + + @override + String get searchGames => 'Buscar partidas'; + + @override + String get searchingGames => 'Buscando partidas cercanas...'; + + @override + String get noGamesFound => 'No se encontraron partidas'; + + @override + String get noGamesFoundHint => + 'Asegúrate de que el host tiene la sala abierta y estáis cerca'; + + @override + String get orScanQR => '¿No aparece? Escanea el QR del host'; } /// The translations for Chinese, as used in Taiwan (`zh_TW`). diff --git a/lib/pantallas/pantalla_unirse.dart b/lib/pantallas/pantalla_unirse.dart index d1905b2..e1c8b6a 100644 --- a/lib/pantallas/pantalla_unirse.dart +++ b/lib/pantallas/pantalla_unirse.dart @@ -5,7 +5,8 @@ import 'package:farolero/l10n/generated/app_localizations.dart'; import '../servicios/servicio_nearby.dart'; import '../tema/tema_app.dart'; -/// Pantalla para unirse a una partida multidispositivo +/// 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}); @@ -16,10 +17,13 @@ class PantallaUnirse extends StatefulWidget { class _PantallaUnirseState extends State { final _nombreController = TextEditingController(); final _formKey = GlobalKey(); - bool _escaneando = false; + + // Estados de la pantalla + bool _buscando = false; + bool _escaneandoQR = false; bool _conectando = false; String? _error; - String? _salaEncontrada; + String? _salaSeleccionada; @override void dispose() { @@ -27,10 +31,51 @@ class _PantallaUnirseState extends State { super.dispose(); } - Future _iniciarEscaneo() async { + /// Paso 1: validar nombre e iniciar discovery + Future _iniciarBusqueda() async { if (!_formKey.currentState!.validate()) 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(() { - _escaneando = true; + _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; }); } @@ -45,20 +90,15 @@ class _PantallaUnirseState extends State { final datos = ServicioNearby.parsearQR(valor); if (datos != null) { setState(() { - _escaneando = false; + _escaneandoQR = false; _conectando = true; - _salaEncontrada = datos['sala'] as String? ?? 'Sala'; + _salaSeleccionada = datos['host'] as String? ?? datos['sala'] as String? ?? 'Sala'; }); - // Iniciar búsqueda de hosts via Nearby + // Iniciar búsqueda para que Nearby encuentre al host final nearby = context.read(); - final ok = await nearby.buscarHosts(_nombreController.text.trim()); - - if (!ok && mounted) { - setState(() { - _conectando = false; - _error = 'No se pudo iniciar la búsqueda. Verifica Bluetooth y ubicación.'; - }); + if (!nearby.buscando) { + await nearby.buscarHosts(_nombreController.text.trim()); } return; } @@ -70,98 +110,194 @@ class _PantallaUnirseState extends State { final l10n = AppLocalizations.of(context)!; final nearby = context.watch(); - // Si estamos conectados, mostrar pantalla de espera + // Si estamos conectados → pantalla de espera if (nearby.conectado && !nearby.esHost) { - return _buildPantallaEspera(context, l10n, nearby); + 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.close), + icon: const Icon(Icons.arrow_back), onPressed: () async { - await nearby.desconectar(); - if (context.mounted) Navigator.pop(context); + await nearby.pararBusqueda(); + setState(() { + _buscando = false; + _conectando = false; + }); }, ), ), - body: _escaneando - ? _buildEscaner(context, l10n) - : _buildFormulario(context, l10n), - ); - } - - Widget _buildFormulario(BuildContext context, AppLocalizations l10n) { - return Padding( - padding: const EdgeInsets.all(32), - child: Form( - key: _formKey, + body: Padding( + padding: const EdgeInsets.all(24), 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.enterNameAndScan, - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - - // Campo nombre - 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, - ), - const SizedBox(height: 24), - + // Estado if (_conectando) ...[ const CircularProgressIndicator(color: TemaApp.colorAcento), const SizedBox(height: 12), Text( - '${l10n.connectingTo} ${_salaEncontrada ?? ""}...', - style: Theme.of(context).textTheme.bodyMedium, + '${l10n.connectingTo} ${_salaSeleccionada ?? ""}...', + style: Theme.of(context).textTheme.bodyLarge, ), + const SizedBox(height: 24), ] else ...[ - // Botón escanear QR - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _iniciarEscaneo, - icon: const Icon(Icons.qr_code_scanner), - label: Text(l10n.scanQR), - ), + // 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) ...[ - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: TemaApp.colorAcento.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), + _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, ), - child: Text( - _error!, - style: const TextStyle(color: TemaApp.colorAcento), - textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _abrirEscaner, + icon: const Icon(Icons.qr_code_scanner), + label: Text(l10n.scanQR), ), ), ], @@ -171,68 +307,108 @@ class _PantallaUnirseState extends State { ); } - Widget _buildEscaner(BuildContext context, AppLocalizations l10n) { - return Stack( - children: [ - MobileScanner( - onDetect: _onQRDetectado, - ), - // Overlay - 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: Column( - mainAxisSize: MainAxisSize.min, + 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: [ - Text( - l10n.scanHostQR, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Colors.white, + 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 SizedBox(height: 16), - OutlinedButton( - onPressed: () => setState(() => _escaneando = false), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, - side: const BorderSide(color: Colors.white), - ), - child: Text(l10n.cancel), - ), + const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey), ], ), ), ), - ], + ), ); } - Widget _buildPantallaEspera( - BuildContext context, - AppLocalizations l10n, - ServicioNearby nearby, - ) { + // ==================== ESCÁNER QR ==================== + + Widget _buildEscaner(BuildContext context, AppLocalizations l10n) { return Scaffold( appBar: AppBar( - title: Text(_salaEncontrada ?? l10n.joinGameTitle), + 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) Navigator.pop(context); + if (context.mounted) { + setState(() { + _buscando = false; + _conectando = false; + }); + } }, ), ), @@ -267,4 +443,21 @@ class _PantallaUnirseState extends State { ), ); } + + // ==================== 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, + ), + ); + } } diff --git a/lib/servicios/servicio_nearby.dart b/lib/servicios/servicio_nearby.dart index 241342d..96a19e6 100644 --- a/lib/servicios/servicio_nearby.dart +++ b/lib/servicios/servicio_nearby.dart @@ -71,6 +71,9 @@ class ServicioNearby extends ChangeNotifier { final Map _jugadores = {}; final List _listeners = []; + // Hosts descubiertos (para discovery automático) + final Map _hostsEncontrados = {}; // endpointId -> nombre + // Estado para clientes String? _palabraRecibida; bool? _soyImpostor; @@ -92,6 +95,7 @@ class ServicioNearby extends ChangeNotifier { List get jugadores => _jugadores.values.toList(); int get numJugadoresConectados => _jugadores.length; + Map get hostsEncontrados => Map.unmodifiable(_hostsEncontrados); /// Registra un listener de mensajes void onMensaje(OnMensajeCallback callback) { @@ -239,12 +243,26 @@ class ServicioNearby extends ChangeNotifier { void _onEndpointEncontrado(String endpointId, String endpointName, String serviceId) { debugPrint('Host encontrado: $endpointName ($endpointId)'); - // Auto-conectar al primer host encontrado - conectarAHost(endpointId, _miNombre ?? 'Jugador'); + _hostsEncontrados[endpointId] = endpointName; + notifyListeners(); } void _onEndpointPerdido(String? endpointId) { debugPrint('Endpoint perdido: $endpointId'); + if (endpointId != null) { + _hostsEncontrados.remove(endpointId); + notifyListeners(); + } + } + + /// Para el discovery sin desconectar + Future pararBusqueda() async { + try { + await Nearby().stopDiscovery(); + } catch (_) {} + _buscando = false; + _hostsEncontrados.clear(); + notifyListeners(); } void _onPayloadRecibido(String endpointId, Payload payload) { @@ -443,6 +461,7 @@ class ServicioNearby extends ChangeNotifier { _faseActual = null; _datosPartida = null; _jugadores.clear(); + _hostsEncontrados.clear(); notifyListeners(); }