diff --git a/lib/pantallas/pantalla_crear_partida.dart b/lib/pantallas/pantalla_crear_partida.dart index 01134dd..4bf6516 100644 --- a/lib/pantallas/pantalla_crear_partida.dart +++ b/lib/pantallas/pantalla_crear_partida.dart @@ -4,7 +4,10 @@ import 'package:provider/provider.dart'; import '../estado/estado_juego.dart'; import '../modelos/palabra.dart'; import '../modelos/partida.dart'; +import '../servicios/servicio_nearby.dart'; +import '../servicios/servicio_permisos.dart'; import '../tema/tema_app.dart'; +import 'pantalla_lobby_host.dart'; import 'pantalla_ver_palabra.dart'; class PantallaCrearPartida extends StatefulWidget { @@ -66,6 +69,12 @@ class _PantallaCrearPartidaState extends State { void _iniciarPartida() { final l10n = AppLocalizations.of(context)!; + + if (_modoMultimovil) { + _iniciarPartidaMulti(); + return; + } + if (_jugadores.length < 3) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.minPlayersRequired)), @@ -76,7 +85,7 @@ class _PantallaCrearPartidaState extends State { final estado = context.read(); estado.crearPartida( config: ConfigPartida( - modoMultimovil: _modoMultimovil, + modoMultimovil: false, categoria: _categoria, numImpostores: _numImpostores, pistaImpostor: _pistaImpostor, @@ -91,6 +100,120 @@ class _PantallaCrearPartidaState extends State { ); } + Future _iniciarPartidaMulti() async { + // 1. Pedir permisos automáticamente + final permisosOk = await ServicioPermisos.solicitarPermisosNearby(context); + if (!permisosOk) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Se necesitan permisos de Bluetooth y ubicación')), + ); + } + return; + } + + // 2. Pedir nombre del host + final nombre = await _pedirNombreHost(); + if (nombre == null || nombre.trim().isEmpty) return; + + // 3. Iniciar host en Nearby + if (!mounted) return; + final nearby = context.read(); + final nombreSala = '${nombre.trim()} - Farolero'; + final ok = await nearby.iniciarHost(nombreSala, nombre.trim()); + + if (!ok) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No se pudo crear la sala. Verifica Bluetooth.')), + ); + } + return; + } + + // 4. Navegar al lobby con QR + if (mounted) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => PantallaLobbyHost( + nombreSala: nombreSala, + onIniciar: () { + // Cuando el host toca "Iniciar" con suficientes jugadores + final estado = context.read(); + final jugadoresMulti = [ + nombre.trim(), + ...nearby.jugadores.map((j) => j.nombre), + ]; + estado.crearPartida( + config: ConfigPartida( + modoMultimovil: true, + categoria: _categoria, + numImpostores: _numImpostores, + pistaImpostor: _pistaImpostor, + tiempoDebateSegundos: _tiempoDebate, + ), + nombresJugadores: jugadoresMulti, + ); + + // Enviar palabras a cada jugador via Nearby + final partida = estado.partida!; + final impostores = {}; + for (int i = 0; i < nearby.jugadores.length; i++) { + final jugadorNearby = nearby.jugadores[i]; + // El jugador [0] es el host, los de nearby son [1..n] + final jugadorPartida = partida.jugadores[i + 1]; + impostores[jugadorNearby.endpointId] = jugadorPartida.esImpostor; + } + + nearby.enviarInicioPartida( + palabraSecreta: partida.palabraSecreta, + categoria: _categoria, + impostores: impostores, + ); + + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const PantallaVerPalabra()), + ); + }, + ), + ), + ); + } + } + + Future _pedirNombreHost() async { + final controller = TextEditingController(); + final l10n = AppLocalizations.of(context)!; + return showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(l10n.yourName), + content: TextField( + controller: controller, + autofocus: true, + textCapitalization: TextCapitalization.words, + decoration: InputDecoration( + hintText: l10n.yourName, + prefixIcon: const Icon(Icons.person), + ), + onSubmitted: (v) => Navigator.pop(ctx, v), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, controller.text), + child: const Text('OK'), + ), + ], + ), + ); + } + @override void dispose() { _controladorNombre.dispose(); @@ -316,7 +439,7 @@ class _PantallaCrearPartidaState extends State { width: double.infinity, height: 56, child: ElevatedButton.icon( - onPressed: _jugadores.length >= 3 ? _iniciarPartida : null, + onPressed: (_modoMultimovil || _jugadores.length >= 3) ? _iniciarPartida : null, icon: const Icon(Icons.play_arrow), label: Text(l10n.startGame), style: ElevatedButton.styleFrom( diff --git a/lib/pantallas/pantalla_unirse.dart b/lib/pantallas/pantalla_unirse.dart index e1c8b6a..81af3cd 100644 --- a/lib/pantallas/pantalla_unirse.dart +++ b/lib/pantallas/pantalla_unirse.dart @@ -3,6 +3,7 @@ import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:provider/provider.dart'; import 'package:farolero/l10n/generated/app_localizations.dart'; import '../servicios/servicio_nearby.dart'; +import '../servicios/servicio_permisos.dart'; import '../tema/tema_app.dart'; /// Pantalla para unirse a una partida multidispositivo. @@ -31,10 +32,20 @@ class _PantallaUnirseState extends State { super.dispose(); } - /// Paso 1: validar nombre e iniciar discovery + /// 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()); diff --git a/lib/servicios/servicio_permisos.dart b/lib/servicios/servicio_permisos.dart new file mode 100644 index 0000000..7457581 --- /dev/null +++ b/lib/servicios/servicio_permisos.dart @@ -0,0 +1,77 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +/// Gestiona los permisos necesarios para Nearby Connections +class ServicioPermisos { + /// Solicita todos los permisos necesarios para el modo multidispositivo. + /// Retorna true si todos los permisos fueron concedidos. + static Future solicitarPermisosNearby(BuildContext context) async { + final permisos = [ + Permission.locationWhenInUse, + ]; + + // Android 12+ necesita permisos BT específicos + if (Platform.isAndroid) { + permisos.addAll([ + Permission.bluetoothAdvertise, + Permission.bluetoothConnect, + Permission.bluetoothScan, + Permission.nearbyWifiDevices, + ]); + } + + // Solicitar todos + final resultados = await permisos.request(); + + // Verificar cuáles fueron denegados + final denegados = []; + for (final entry in resultados.entries) { + if (!entry.value.isGranted) { + denegados.add(_nombrePermiso(entry.key)); + } + } + + if (denegados.isEmpty) return true; + + // Mostrar diálogo con los permisos que faltan + if (context.mounted) { + final abrirConfig = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Permisos necesarios'), + content: Text( + 'Para el modo multijugador necesitamos:\n\n' + '${denegados.map((d) => '• $d').join('\n')}\n\n' + '¿Abrir configuración de la app?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Cancelar'), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Abrir configuración'), + ), + ], + ), + ); + + if (abrirConfig == true) { + await openAppSettings(); + } + } + + return false; + } + + static String _nombrePermiso(Permission p) { + if (p == Permission.locationWhenInUse) return 'Ubicación'; + if (p == Permission.bluetoothAdvertise) return 'Bluetooth (anunciar)'; + if (p == Permission.bluetoothConnect) return 'Bluetooth (conectar)'; + if (p == Permission.bluetoothScan) return 'Bluetooth (escanear)'; + if (p == Permission.nearbyWifiDevices) return 'Wi-Fi cercano'; + return p.toString(); + } +} diff --git a/pubspec.lock b/pubspec.lock index 59ac96d..f0fd52b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -317,6 +317,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index abddd6a..df6bb8b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: mobile_scanner: ^6.0.5 google_fonts: ^6.2.1 nearby_connections: ^4.0.0 + permission_handler: ^12.0.1 dev_dependencies: flutter_test: