feat(multi-device): host puede participar como jugador

- Añadido modelo Usuario con pool de usuarios sincronizado
- El host ahora recibe palabra y rol como cualquier jugador
- UI de selección de perfil en pantallas de lobby
- Los clientes pueden ver usuarios del servidor o crear nuevos
- El juego no inicia hasta que el host selecciona perfil
This commit is contained in:
ShanaiaBot
2026-04-24 18:47:56 +02:00
parent 3df3ae1e95
commit d3fc3386f9
31 changed files with 1266 additions and 106 deletions

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:farolero/l10n/generated/app_localizations.dart';
import '../modelos/usuario.dart';
import '../servicios/servicio_nearby.dart';
import '../tema/tema_app.dart';
@@ -22,6 +23,7 @@ class PantallaLobbyHost extends StatefulWidget {
class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
bool _iniciando = false;
String? _perfilSeleccionado;
@override
Widget build(BuildContext context) {
@@ -66,6 +68,65 @@ class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
),
const SizedBox(height: 24),
// Selección de perfil
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.selectYourProfile,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
value: _perfilSeleccionado,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.person),
hintText: l10n.selectProfile,
border: const OutlineInputBorder(),
),
items: [
// Opción para crear nuevo usuario
DropdownMenuItem<String>(
value: '_new_',
child: Row(
children: [
const Icon(Icons.add, size: 18),
const SizedBox(width: 8),
Text(l10n.createNewUser),
],
),
),
// Usuarios existentes
...nearby.usuarios.map((usuario) {
return DropdownMenuItem<String>(
value: usuario.nombre,
child: Row(
children: [
Text(usuario.avatar ?? '👤'),
const SizedBox(width: 8),
Text(usuario.nombre),
],
),
);
}),
],
onChanged: (valor) {
if (valor == '_new_') {
_crearNuevoUsuario(context);
} else {
setState(() => _perfilSeleccionado = valor);
}
},
),
],
),
),
),
const SizedBox(height: 16),
// Lista de jugadores
Expanded(
child: Column(
@@ -116,7 +177,10 @@ class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('📱', style: TextStyle(fontSize: 48)),
const Text(
'📱',
style: TextStyle(fontSize: 48),
),
const SizedBox(height: 12),
Text(
l10n.waitingForPlayers,
@@ -141,15 +205,26 @@ class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
if (totalJugadores < 3)
Text(
l10n.needMorePlayers(3 - totalJugadores),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: TemaApp.colorNaranja,
),
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: TemaApp.colorNaranja),
),
const SizedBox(height: 12),
if (_perfilSeleccionado == null)
Text(
l10n.selectProfile,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: TemaApp.colorNaranja),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: totalJugadores >= 3 && !_iniciando
onPressed:
totalJugadores >= 3 &&
_perfilSeleccionado != null &&
!_iniciando
? () {
setState(() => _iniciando = true);
widget.onIniciar();
@@ -181,10 +256,7 @@ class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
Text(esHost ? '👑' : '🎭', style: const TextStyle(fontSize: 20)),
const SizedBox(width: 12),
Expanded(
child: Text(
nombre,
style: Theme.of(context).textTheme.titleMedium,
),
child: Text(nombre, style: Theme.of(context).textTheme.titleMedium),
),
if (esHost)
Container(
@@ -206,4 +278,46 @@ class _PantallaLobbyHostState extends State<PantallaLobbyHost> {
),
);
}
Future<void> _crearNuevoUsuario(BuildContext context) async {
final l10n = AppLocalizations.of(context)!;
final controller = TextEditingController();
final nearby = context.read<ServicioNearby>();
final nombre = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(l10n.createNewUser),
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: Text(l10n.accept),
),
],
),
);
if (nombre != null && nombre.trim().isNotEmpty) {
final nuevoUsuario = Usuario(
id: DateTime.now().millisecondsSinceEpoch.toString(),
nombre: nombre.trim(),
);
nearby.agregarUsuario(nuevoUsuario);
setState(() => _perfilSeleccionado = nombre.trim());
}
}
}