Play rejected version code 54 for still targeting 34. From API 35 Android forces edge-to-edge: content is drawn behind the system bars and it is up to each screen to keep clear of them. Seven screens had no SafeArea and their bottom action button would have ended up under the gesture bar, so FondoFarolero gains a respetarBarras flag that insets the content while the background still paints full bleed. The seventeen screens that already handle it are untouched. The navigation bar colour is ignored under edge-to-edge, so its icons are now forced light to stay visible over the dark background. Verified on the merged manifest: targetSdkVersion 35, and the release bundle builds. The visual result has not been checked on a device.
1305 lines
44 KiB
Dart
1305 lines
44 KiB
Dart
import 'dart:async';
|
|
import 'dart:math';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:farolero/l10n/generated/app_localizations.dart';
|
|
import 'package:provider/provider.dart';
|
|
import '../estado/estado_juego.dart';
|
|
import '../modelos/gamificacion_usuario.dart';
|
|
import '../modelos/inicio_partida_multijugador.dart';
|
|
import '../modelos/jugador.dart';
|
|
import '../modelos/palabra.dart';
|
|
import '../modelos/partida.dart';
|
|
import '../modelos/sala_multijugador.dart';
|
|
import '../modelos/snapshot_partida_online.dart';
|
|
import '../servicios/servicio_historial_partidas.dart';
|
|
import '../servicios/servicio_nearby.dart';
|
|
import '../servicios/servicio_perfil_usuario.dart';
|
|
import '../tema/componentes_farolero.dart';
|
|
import '../tema/componentes_resultado_farolero.dart';
|
|
import '../tema/tema_app.dart';
|
|
import 'pantalla_notas_online.dart';
|
|
import 'pantalla_revision_palabra.dart';
|
|
import 'pantalla_votacion_cliente.dart';
|
|
import 'pantalla_palabras_cliente.dart';
|
|
|
|
class PantallaGestorHost extends StatefulWidget {
|
|
final VoidCallback onPartidaFin;
|
|
|
|
const PantallaGestorHost({super.key, required this.onPartidaFin});
|
|
|
|
@override
|
|
State<PantallaGestorHost> createState() => _PantallaGestorHostState();
|
|
}
|
|
|
|
class _PantallaGestorHostState extends State<PantallaGestorHost> {
|
|
Timer? _timer;
|
|
int _segundosRestantes = 0;
|
|
bool _hostListo = false;
|
|
bool _partidaOnlineGuardada = false;
|
|
ProgresoGamificacionUsuario? _progresoGamificacion;
|
|
String? _primerTurnoId;
|
|
String? _primerTurnoNombre;
|
|
final Map<String, bool> _clientesListos = {};
|
|
final Map<String, String> _votosRecibidos = {};
|
|
OnMensajeCallback? _listenerMensajes;
|
|
ServicioNearby? _nearby;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_iniciarTemporizador();
|
|
_registrarListeners();
|
|
}
|
|
|
|
void _iniciarTemporizador() {
|
|
final estado = context.read<EstadoJuego>();
|
|
final tiempo = estado.partida?.config.tiempoDebateSegundos;
|
|
if (tiempo != null) {
|
|
_segundosRestantes = tiempo;
|
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
if (_segundosRestantes > 0) {
|
|
setState(() => _segundosRestantes--);
|
|
} else {
|
|
timer.cancel();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void _registrarListeners() {
|
|
final nearby = context.read<ServicioNearby>();
|
|
_nearby = nearby;
|
|
_listenerMensajes = (endpointId, mensaje) {
|
|
if (!mounted) return;
|
|
if (mensaje.tipo == TipoMensaje.listo) {
|
|
// Se indexa por clientId, no por endpointId: el endpoint cambia en
|
|
// cada reconexión y si no perderíamos el "ya la he visto".
|
|
final clientId = _clientIdDe(endpointId) ?? endpointId;
|
|
setState(() => _clientesListos[clientId] = true);
|
|
} else if (mensaje.tipo == TipoMensaje.unirse) {
|
|
// Con la partida ya en marcha, un `unirse` solo puede ser un móvil
|
|
// que vuelve tras caerse.
|
|
final nombre = mensaje.datos['nombre'] as String?;
|
|
if (nombre != null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(AppLocalizations.of(context)!.playerRejoined(nombre)),
|
|
),
|
|
);
|
|
}
|
|
} else if (mensaje.tipo == TipoMensaje.solicitarResync) {
|
|
_responderResync(endpointId);
|
|
} else if (mensaje.tipo == TipoMensaje.voto) {
|
|
final votanteId = mensaje.datos['votanteId'] as String?;
|
|
final votoId =
|
|
mensaje.datos['votadoId'] as String? ??
|
|
mensaje.datos['votoporId'] as String?;
|
|
if (votanteId == null || votoId == null) return;
|
|
// Un jugador eliminado ya no vota, venga de donde venga el mensaje.
|
|
final partida = context.read<EstadoJuego>().partida;
|
|
final sigueVivo =
|
|
partida?.jugadoresActivos.any((j) => j.id == votanteId) ?? false;
|
|
if (!sigueVivo) return;
|
|
context.read<EstadoJuego>().registrarVoto(votanteId, votoId);
|
|
setState(() => _votosRecibidos[votanteId] = votoId);
|
|
}
|
|
};
|
|
nearby.onMensaje(_listenerMensajes!);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
final listener = _listenerMensajes;
|
|
if (listener != null) _nearby?.removeMensajeListener(listener);
|
|
super.dispose();
|
|
}
|
|
|
|
String? _clientIdDe(String endpointId) {
|
|
return context
|
|
.read<ServicioNearby>()
|
|
.estadoSala
|
|
?.clientePorEndpoint(endpointId)
|
|
?.clientId;
|
|
}
|
|
|
|
/// Jugadores de la partida que controla un cliente concreto, con su palabra,
|
|
/// su rol y sus compañeros. Es lo que necesita un móvil para volver a jugar.
|
|
List<JugadorInicioPartida> _jugadoresDeCliente(
|
|
Partida partida,
|
|
EstadoSalaMultijugador sala,
|
|
String clientId,
|
|
) {
|
|
final estado = context.read<EstadoJuego>();
|
|
return sala
|
|
.usuariosPorCliente(clientId)
|
|
.where((usuario) => partida.jugadores.any((j) => j.id == usuario.id))
|
|
.map((usuario) {
|
|
final jugador = partida.jugadores.firstWhere(
|
|
(j) => j.id == usuario.id,
|
|
);
|
|
return JugadorInicioPartida(
|
|
jugadorId: jugador.id,
|
|
nombre: jugador.nombre,
|
|
esImpostor: jugador.esImpostor,
|
|
palabra: jugador.esImpostor ? null : partida.palabraSecreta,
|
|
companerosImpostores: estado.companerosImpostoresDe(jugador.id),
|
|
);
|
|
})
|
|
.toList();
|
|
}
|
|
|
|
/// Responde a un móvil que vuelve tras una caída con el estado completo:
|
|
/// en qué fase va la partida y qué jugadores le tocan.
|
|
Future<void> _responderResync(String endpointId) async {
|
|
final nearby = context.read<ServicioNearby>();
|
|
final estado = context.read<EstadoJuego>();
|
|
final partida = estado.partida;
|
|
final sala = nearby.estadoSala;
|
|
if (partida == null || sala == null) return;
|
|
|
|
final clientId = _clientIdDe(endpointId);
|
|
if (clientId == null) return;
|
|
|
|
final datos = _snapshot(fase: partida.fase.name).toJson();
|
|
datos['fase'] = partida.fase.name;
|
|
datos['jugadores'] = _jugadoresDeCliente(partida, sala, clientId)
|
|
.map((jugador) => jugador.toJson())
|
|
.toList();
|
|
if (partida.config.pistaImpostor) {
|
|
datos['pistaImpostor'] = partida.pistaImpostor;
|
|
}
|
|
if (partida.config.tiempoDebateSegundos != null) {
|
|
datos['tiempoDebateSegundos'] = partida.config.tiempoDebateSegundos;
|
|
}
|
|
if (_primerTurnoNombre != null) {
|
|
datos['primerTurnoNombre'] = _primerTurnoNombre;
|
|
}
|
|
|
|
await nearby.enviarResync(endpointId, datos);
|
|
if (mounted) setState(() {});
|
|
}
|
|
|
|
String _formatearTiempo(int segundos) {
|
|
final min = segundos ~/ 60;
|
|
final seg = segundos % 60;
|
|
return "${min.toString().padLeft(2, '0')}:${seg.toString().padLeft(2, '0')}";
|
|
}
|
|
|
|
void _avanzarAFase(FaseJuego fase) {
|
|
final estado = context.read<EstadoJuego>();
|
|
final nearby = context.read<ServicioNearby>();
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
switch (fase) {
|
|
case FaseJuego.debate:
|
|
estado.iniciarDebate();
|
|
final primero = _elegirPrimerTurno();
|
|
nearby.enviarCambioFase('debate', {
|
|
..._snapshot(fase: 'debate').toJson(),
|
|
if (primero != null) ...{
|
|
'primerTurnoId': primero.id,
|
|
'primerTurnoNombre': primero.nombre,
|
|
},
|
|
if (estado.partida?.config.tiempoDebateSegundos != null)
|
|
'tiempoDebateSegundos': estado.partida!.config.tiempoDebateSegundos,
|
|
});
|
|
_iniciarTemporizador();
|
|
break;
|
|
case FaseJuego.votacion:
|
|
estado.iniciarVotacion();
|
|
nearby.enviarCambioFase('votacion', _snapshot(fase: 'votacion').toJson());
|
|
_votosRecibidos.clear();
|
|
break;
|
|
case FaseJuego.resultado:
|
|
final resultado = estado.procesarVotacion();
|
|
if (resultado != null) {
|
|
nearby.enviarResultadoVotacion(
|
|
_snapshot(
|
|
fase: 'resultado',
|
|
resultadoActual: resultado,
|
|
mensaje: _mensajeSiguienteAccion(estado, resultado, l10n),
|
|
).toJson(),
|
|
);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final estado = context.watch<EstadoJuego>();
|
|
final nearby = context.watch<ServicioNearby>();
|
|
final partida = estado.partida;
|
|
|
|
if (partida == null) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(l10n.hostGame)),
|
|
body: FondoFarolero(
|
|
intenso: true,
|
|
child: Center(child: Text(l10n.errorNoGame)),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Solo bloquean los clientes que siguen conectados: un móvil caído no
|
|
// puede impedir que avance la partida.
|
|
final clientesPendientes =
|
|
nearby.estadoSala?.clientes.values
|
|
.where((cliente) => !cliente.esHost && cliente.conectado)
|
|
.map((cliente) => cliente.clientId) ??
|
|
const <String>[];
|
|
final todosListos =
|
|
_hostListo &&
|
|
clientesPendientes.every((id) => _clientesListos[id] == true);
|
|
final todosVotaron = estado.todosHanVotado();
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(l10n.hostGame),
|
|
automaticallyImplyLeading: false,
|
|
actions: [
|
|
IconButton(
|
|
tooltip: l10n.seeYourWord,
|
|
icon: IconoFarolero(Icons.visibility),
|
|
onPressed: partida.fase.index <= FaseJuego.verPalabra.index
|
|
? null
|
|
: () => mostrarRevisionPalabraOnline(
|
|
context: context,
|
|
jugadoresControlados: _jugadoresHostControlados(
|
|
partida,
|
|
nearby,
|
|
),
|
|
pistaImpostor: partida.config.pistaImpostor
|
|
? partida.pistaImpostor
|
|
: null,
|
|
),
|
|
),
|
|
IconButton(
|
|
tooltip: l10n.notesTitle,
|
|
icon: IconoFarolero(Icons.edit_note),
|
|
onPressed: partida.fase.index < FaseJuego.debate.index ||
|
|
nearby.roomId == null
|
|
? null
|
|
: () => Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => PantallaNotasOnline(
|
|
partidaId: nearby.roomId!,
|
|
jugadores: partida.jugadoresActivos,
|
|
autoresControlados: _jugadoresHostControlados(
|
|
partida,
|
|
nearby,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: IconoFarolero(Icons.close),
|
|
onPressed: () async {
|
|
await nearby.desconectar();
|
|
widget.onPartidaFin();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: FondoFarolero(
|
|
intenso: true,
|
|
respetarBarras: true,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
_buildAvisoClientesDesconectados(context, nearby),
|
|
_buildFaseIndicator(context, partida.fase, l10n),
|
|
const SizedBox(height: 8),
|
|
const ArteGameplayFarolero.fase(height: 92),
|
|
const SizedBox(height: 16),
|
|
Expanded(
|
|
child: _buildContenidoFase(
|
|
context,
|
|
partida.fase,
|
|
l10n,
|
|
todosListos,
|
|
todosVotaron,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildBotonAccion(
|
|
context,
|
|
partida.fase,
|
|
l10n,
|
|
todosListos,
|
|
todosVotaron,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAvisoClientesDesconectados(
|
|
BuildContext context,
|
|
ServicioNearby nearby,
|
|
) {
|
|
final sala = nearby.estadoSala;
|
|
final usuariosAfectados = sala?.usuariosDeClientesDesconectados ?? const [];
|
|
if (usuariosAfectados.isEmpty) return const SizedBox.shrink();
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: TemaApp.decoracionPanel(
|
|
color: TemaApp.colorAcento.withValues(alpha: 0.16),
|
|
borderColor: TemaApp.colorAcento.withValues(alpha: 0.65),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
IconoFarolero(Icons.link_off, color: TemaApp.colorAcento),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
AppLocalizations.of(context)!.disconnectedPlayersWarning,
|
|
style: Theme.of(context).textTheme.titleSmall,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
usuariosAfectados.map((usuario) => usuario.nombre).join(', '),
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: SizedBox(
|
|
width: 260,
|
|
child: BotonFarolero.oscuro(
|
|
texto: AppLocalizations.of(context)!.assumeOnThisPhone,
|
|
icono: Icons.person_add_alt_1,
|
|
assetIconPath: 'assets/ui/generated/actions/action_add_player.webp',
|
|
onPressed: () async {
|
|
// Son jugadores nuevos para este móvil: sus palabras están
|
|
// sin ver y sus votos sin emitir.
|
|
final fase = context.read<EstadoJuego>().partida?.fase;
|
|
await nearby.asumirUsuariosDesconectados();
|
|
if (!mounted) return;
|
|
setState(() {
|
|
if (fase == FaseJuego.verPalabra) _hostListo = false;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFaseIndicator(
|
|
BuildContext context,
|
|
FaseJuego fase,
|
|
AppLocalizations l10n,
|
|
) {
|
|
final fases = [
|
|
(FaseJuego.verPalabra, l10n.seeYourWord),
|
|
(FaseJuego.debate, l10n.debate),
|
|
(FaseJuego.votacion, l10n.voting),
|
|
(FaseJuego.resultado, l10n.result),
|
|
(FaseJuego.adivinanza, l10n.guess),
|
|
(FaseJuego.finPartida, l10n.gameOver),
|
|
];
|
|
|
|
return SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: fases.map((e) {
|
|
final esActiva = fase == e.$1 || fase.index > e.$1.index;
|
|
return Container(
|
|
margin: const EdgeInsets.only(right: 8),
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: esActiva ? TemaApp.colorAcento : TemaApp.colorSuperficie,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
e.$2,
|
|
style: TextStyle(
|
|
color: esActiva ? Colors.white : TemaApp.colorTextoSecundario,
|
|
fontWeight: esActiva ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildContenidoFase(
|
|
BuildContext context,
|
|
FaseJuego fase,
|
|
AppLocalizations l10n,
|
|
bool todosListos,
|
|
bool todosVotaron,
|
|
) {
|
|
final nearby = context.watch<ServicioNearby>();
|
|
|
|
switch (fase) {
|
|
case FaseJuego.verPalabra:
|
|
return _buildFaseVerPalabra(context, l10n, todosListos, nearby);
|
|
case FaseJuego.debate:
|
|
return _buildFaseDebate(context, l10n, nearby);
|
|
case FaseJuego.votacion:
|
|
return _buildFaseVotacion(context, l10n, todosVotaron, nearby);
|
|
case FaseJuego.resultado:
|
|
return _buildFaseResultado(context, l10n);
|
|
case FaseJuego.adivinanza:
|
|
return _buildFaseAdivinanza(context, l10n);
|
|
case FaseJuego.finPartida:
|
|
return _buildFaseFinOnline(context, l10n);
|
|
default:
|
|
return Center(child: Text(l10n.gameOver));
|
|
}
|
|
}
|
|
|
|
SnapshotPartidaOnline _snapshot({
|
|
required String fase,
|
|
ResultadoVotacion? resultadoActual,
|
|
String? mensaje,
|
|
bool revelarFinal = false,
|
|
}) {
|
|
final estado = context.read<EstadoJuego>();
|
|
final nearby = context.read<ServicioNearby>();
|
|
final partida = estado.partida!;
|
|
return SnapshotPartidaOnline.desdePartida(
|
|
partida,
|
|
roomId: nearby.roomId,
|
|
fase: fase,
|
|
resultadoActual: resultadoActual,
|
|
mensaje: mensaje,
|
|
revelarPalabra: revelarFinal,
|
|
revelarImpostores: revelarFinal,
|
|
);
|
|
}
|
|
|
|
String _mensajeSiguienteAccion(
|
|
EstadoJuego estado,
|
|
ResultadoVotacion resultado,
|
|
AppLocalizations l10n,
|
|
) {
|
|
final partida = estado.partida;
|
|
if (partida != null && _hayFinTrasVotacion(partida)) {
|
|
return l10n.gameOver;
|
|
}
|
|
if (resultado.eraImpostor) {
|
|
return l10n.impostorCanGuess.replaceAll('\n', ' ');
|
|
}
|
|
return l10n.gameContinues;
|
|
}
|
|
|
|
bool _hayFinTrasVotacion(Partida partida) {
|
|
final impostoresVivos = partida.impostoresActivos.length;
|
|
final jugadoresVivos = partida.jugadoresNormalesActivos.length;
|
|
return impostoresVivos == 0 || impostoresVivos >= jugadoresVivos;
|
|
}
|
|
|
|
Widget _buildFaseVerPalabra(
|
|
BuildContext context,
|
|
AppLocalizations l10n,
|
|
bool todosListos,
|
|
ServicioNearby nearby,
|
|
) {
|
|
return TarjetaFaseFarolero(
|
|
icono: Icons.visibility,
|
|
assetIconPath: 'assets/ui/generated/actions/action_reveal_word.webp',
|
|
titulo: l10n.waitingPlayersSeeWord,
|
|
subtitulo: l10n.connectedPlayers,
|
|
child: Column(
|
|
children: [
|
|
_buildJugadorTile(nearby.miNombre ?? 'Host', true, _hostListo),
|
|
...nearby.jugadores.map(
|
|
(jugador) => _buildJugadorTile(
|
|
jugador.nombre,
|
|
false,
|
|
_clientesListos[_clientIdDe(jugador.endpointId) ??
|
|
jugador.endpointId] ??
|
|
false,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
BotonFarolero(
|
|
texto: l10n.seeYourWord,
|
|
icono: Icons.visibility,
|
|
assetIconPath: 'assets/ui/generated/actions/action_reveal_word.webp',
|
|
onPressed: () => _mostrarPalabraHost(context),
|
|
),
|
|
if (todosListos) ...[
|
|
const SizedBox(height: 12),
|
|
EstadoJugadorFarolero(
|
|
nombre: l10n.allSeenStartDebate,
|
|
completado: true,
|
|
icono: Icons.check_circle,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
List<JugadorInicioPartida> _jugadoresHostControlados(
|
|
Partida partida,
|
|
ServicioNearby nearby,
|
|
) {
|
|
final sala = nearby.estadoSala;
|
|
if (sala == null) return const [];
|
|
return _jugadoresDeCliente(partida, sala, sala.hostClientId);
|
|
}
|
|
|
|
void _mostrarPalabraHost(BuildContext context) {
|
|
final estado = context.read<EstadoJuego>();
|
|
final nearby = context.read<ServicioNearby>();
|
|
final partida = estado.partida;
|
|
if (partida == null) return;
|
|
|
|
final jugadoresHost = _jugadoresHostControlados(partida, nearby);
|
|
|
|
if (jugadoresHost.length > 1) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => PantallaPalabrasCliente(
|
|
jugadores: jugadoresHost,
|
|
pistaImpostor: partida.config.pistaImpostor
|
|
? partida.pistaImpostor
|
|
: null,
|
|
onTodosVistos: () {
|
|
setState(() => _hostListo = true);
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
final hostLocal = jugadoresHost.isNotEmpty
|
|
? partida.jugadores.firstWhere((j) => j.id == jugadoresHost.first.jugadorId)
|
|
: partida.jugadores.first;
|
|
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => _PantallaRevelarPalabraHost(
|
|
nombre: hostLocal.nombre,
|
|
esImpostor: hostLocal.esImpostor,
|
|
palabra: partida.palabraSecreta,
|
|
pistaActiva: partida.config.pistaImpostor,
|
|
pista: partida.pistaImpostor,
|
|
companerosImpostores: estado.companerosImpostoresDe(hostLocal.id),
|
|
onVisto: () => setState(() => _hostListo = true),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Jugador? _elegirPrimerTurno() {
|
|
final partida = context.read<EstadoJuego>().partida;
|
|
if (partida == null || partida.jugadoresActivos.isEmpty) return null;
|
|
if (_primerTurnoId != null) {
|
|
return partida.jugadoresActivos.firstWhere(
|
|
(j) => j.id == _primerTurnoId,
|
|
orElse: () => partida.jugadoresActivos.first,
|
|
);
|
|
}
|
|
final elegido = partida.jugadoresActivos[
|
|
Random.secure().nextInt(partida.jugadoresActivos.length)];
|
|
_primerTurnoId = elegido.id;
|
|
_primerTurnoNombre = elegido.nombre;
|
|
return elegido;
|
|
}
|
|
|
|
Widget _buildFaseDebate(
|
|
BuildContext context,
|
|
AppLocalizations l10n,
|
|
ServicioNearby nearby,
|
|
) {
|
|
final estado = context.read<EstadoJuego>();
|
|
final tiempo = estado.partida?.config.tiempoDebateSegundos;
|
|
|
|
return TarjetaFaseFarolero(
|
|
icono: Icons.forum,
|
|
assetIconPath: 'assets/ui/generated/actions/action_rules_book.webp',
|
|
titulo: l10n.debate,
|
|
subtitulo: l10n.debateInstructions,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (tiempo != null) ...[
|
|
TemporizadorFarolero(
|
|
etiqueta: _segundosRestantes == 0
|
|
? l10n.timeUp
|
|
: l10n.timeRemaining,
|
|
tiempo: _formatearTiempo(_segundosRestantes),
|
|
agotado: _segundosRestantes == 0,
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
_buildPrimerTurno(context),
|
|
const SizedBox(height: 16),
|
|
Text(l10n.activePlayers, style: Theme.of(context).textTheme.titleMedium),
|
|
const SizedBox(height: 8),
|
|
_buildJugadorTile(nearby.miNombre ?? 'Host', true, true),
|
|
...nearby.jugadores.map(
|
|
(jugador) => _buildJugadorTile(jugador.nombre, false, true),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPrimerTurno(BuildContext context) {
|
|
final primero = _elegirPrimerTurno();
|
|
final nombre = _primerTurnoNombre ?? primero?.nombre;
|
|
if (nombre == null) return const SizedBox.shrink();
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: TemaApp.decoracionPanel(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.16),
|
|
borderColor: TemaApp.colorNaranja.withValues(alpha: 0.7),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
IconoFarolero(Icons.record_voice_over, color: TemaApp.colorNaranja),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
AppLocalizations.of(context)!.firstTurnInstruction(nombre),
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
Widget _buildFaseVotacion(
|
|
BuildContext context,
|
|
AppLocalizations l10n,
|
|
bool todosVotaron,
|
|
ServicioNearby nearby,
|
|
) {
|
|
final estado = context.watch<EstadoJuego>();
|
|
final partida = estado.partida!;
|
|
final totalVotos = partida.jugadoresActivos.length;
|
|
final votosEmitidos = estado.votos.length;
|
|
final progreso = totalVotos == 0 ? 0.0 : votosEmitidos / totalVotos;
|
|
|
|
return TarjetaFaseFarolero(
|
|
icono: Icons.how_to_vote,
|
|
assetIconPath: 'assets/ui/generated/actions/action_vote_mask.webp',
|
|
titulo: l10n.voting,
|
|
subtitulo: l10n.votesProgress(votosEmitidos, totalVotos),
|
|
color: TemaApp.colorAcento,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(999),
|
|
child: LinearProgressIndicator(
|
|
value: progreso.clamp(0.0, 1.0).toDouble(),
|
|
minHeight: 14,
|
|
backgroundColor: Colors.black.withValues(alpha: 0.35),
|
|
valueColor: const AlwaysStoppedAnimation(TemaApp.colorAcento),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
if (!_hostYaVoto(context))
|
|
BotonFarolero.secundario(
|
|
texto: l10n.votar,
|
|
icono: Icons.how_to_vote,
|
|
onPressed: () => _abrirVotacionHost(context),
|
|
),
|
|
if (!_hostYaVoto(context)) const SizedBox(height: 16),
|
|
...partida.jugadoresActivos.map((jugador) {
|
|
final haVotado = estado.votos.containsKey(jugador.id);
|
|
return _buildJugadorTile(jugador.nombre, false, haVotado);
|
|
}),
|
|
if (todosVotaron)
|
|
EstadoJugadorFarolero(
|
|
nombre: l10n.allVoted,
|
|
completado: true,
|
|
icono: Icons.check_circle,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFaseResultado(BuildContext context, AppLocalizations l10n) {
|
|
final partida = context.watch<EstadoJuego>().partida;
|
|
final resultado = partida?.historialVotaciones.isNotEmpty == true
|
|
? partida!.historialVotaciones.last
|
|
: null;
|
|
if (partida == null || resultado == null) {
|
|
return Center(child: Text(l10n.noResult));
|
|
}
|
|
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: ResultadoRondaFarolero(
|
|
resultado: resultado,
|
|
jugadores: partida.jugadores,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFaseAdivinanza(BuildContext context, AppLocalizations l10n) {
|
|
final partida = context.watch<EstadoJuego>().partida;
|
|
final ultimo = partida?.historialVotaciones.isNotEmpty == true
|
|
? partida!.historialVotaciones.last
|
|
: null;
|
|
return TarjetaFaseFarolero(
|
|
icono: Icons.psychology,
|
|
assetIconPath: 'assets/ui/generated/actions/action_impostor_mask.webp',
|
|
titulo: l10n.impostorGuessTitle,
|
|
subtitulo: ultimo == null
|
|
? l10n.impostorCanGuess
|
|
: '${ultimo.eliminadoNombre}: ${l10n.impostorCanGuess}',
|
|
color: TemaApp.colorNaranja,
|
|
child: const ArteGameplayFarolero.resultado(height: 132),
|
|
);
|
|
}
|
|
|
|
Widget _buildFaseFinOnline(BuildContext context, AppLocalizations l10n) {
|
|
final partida = context.watch<EstadoJuego>().partida;
|
|
if (partida == null) return Center(child: Text(l10n.noResult));
|
|
|
|
final ganaronJugadores = partida.ganador == 'jugadores';
|
|
final color = ganaronJugadores ? TemaApp.colorVerde : TemaApp.colorAcento;
|
|
final impostores = partida.jugadores.where((j) => j.esImpostor).toList();
|
|
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Column(
|
|
children: [
|
|
HeroFinalPartidaFarolero(
|
|
encabezado: l10n.gameOver,
|
|
titulo: ganaronJugadores ? l10n.playersWin : l10n.impostorsWin,
|
|
icono: ganaronJugadores ? Icons.emoji_events : Icons.theater_comedy,
|
|
color: color,
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (_progresoGamificacion == null)
|
|
const TarjetaRecompensaCargandoPremium()
|
|
else
|
|
TarjetaProgresoGamificacionPremium(
|
|
progreso: _progresoGamificacion!,
|
|
),
|
|
const SizedBox(height: 18),
|
|
TarjetaSecretoPremium(
|
|
palabra: partida.palabraSecreta,
|
|
categoria: BancoPalabras.nombreBonitoCategoria(
|
|
partida.categoriaReal,
|
|
l10n,
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
TarjetaImpostoresPremium(
|
|
titulo: impostores.length == 1
|
|
? l10n.theImpostorWas
|
|
: l10n.theImpostorsWere,
|
|
impostores: impostores,
|
|
),
|
|
const SizedBox(height: 18),
|
|
if (partida.historialVotaciones.isNotEmpty)
|
|
TarjetaHistorialVotosPremium(
|
|
historial: partida.historialVotaciones,
|
|
jugadores: partida.jugadores,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
bool _hostYaVoto(BuildContext context) {
|
|
final estado = context.read<EstadoJuego>();
|
|
final sala = context.read<ServicioNearby>().estadoSala;
|
|
if (sala == null || estado.partida == null) return false;
|
|
final hostIds = sala.usuariosPorCliente(sala.hostClientId).map((u) => u.id);
|
|
return hostIds.every((id) => estado.votos.containsKey(id));
|
|
}
|
|
|
|
void _abrirVotacionHost(BuildContext context) {
|
|
final estado = context.read<EstadoJuego>();
|
|
final sala = context.read<ServicioNearby>().estadoSala;
|
|
final partida = estado.partida;
|
|
if (sala == null || partida == null) return;
|
|
|
|
final jugadoresHost = sala.usuariosPorCliente(sala.hostClientId)
|
|
.where((usuario) => partida.jugadoresActivos.any((j) => j.id == usuario.id))
|
|
.map(
|
|
(usuario) => JugadorInicioPartida(
|
|
jugadorId: usuario.id,
|
|
nombre: usuario.nombre,
|
|
esImpostor: partida.jugadores.firstWhere((j) => j.id == usuario.id).esImpostor,
|
|
palabra: partida.jugadores.firstWhere((j) => j.id == usuario.id).palabra,
|
|
),
|
|
)
|
|
.toList();
|
|
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => PantallaVotacionCliente(
|
|
jugadores: partida.jugadoresActivos,
|
|
jugadoresControlados: jugadoresHost,
|
|
partidaId: context.read<ServicioNearby>().roomId,
|
|
pistaImpostor: partida.config.pistaImpostor
|
|
? partida.pistaImpostor
|
|
: null,
|
|
onVotos: (votos) {
|
|
for (final entry in votos.entries) {
|
|
estado.registrarVoto(entry.key, entry.value);
|
|
_votosRecibidos[entry.key] = entry.value;
|
|
}
|
|
if (mounted) setState(() {});
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildJugadorTile(String nombre, bool esHost, bool listo) {
|
|
return EstadoJugadorFarolero(
|
|
nombre: nombre,
|
|
destacado: esHost,
|
|
completado: listo,
|
|
icono: esHost ? Icons.phone_android : Icons.devices,
|
|
assetIconPath: esHost ? 'assets/ui/generated/actions/action_mobile_device.webp' : 'assets/ui/generated/actions/action_multidevice_signal.webp',
|
|
);
|
|
}
|
|
|
|
Widget _buildBotonAccion(
|
|
BuildContext context,
|
|
FaseJuego fase,
|
|
AppLocalizations l10n,
|
|
bool todosListos,
|
|
bool todosVotaron,
|
|
) {
|
|
switch (fase) {
|
|
case FaseJuego.verPalabra:
|
|
return BotonFarolero(
|
|
texto: todosListos ? l10n.allSeenStartDebate : l10n.waitingPlayersSeeWord,
|
|
icono: Icons.forum,
|
|
assetIconPath: 'assets/ui/generated/actions/action_rules_book.webp',
|
|
onPressed: todosListos ? () => _avanzarAFase(FaseJuego.debate) : null,
|
|
);
|
|
case FaseJuego.debate:
|
|
return BotonFarolero.secundario(
|
|
texto: l10n.goToVoting,
|
|
icono: Icons.how_to_vote,
|
|
assetIconPath: 'assets/ui/generated/actions/action_vote_mask.webp',
|
|
onPressed: () => _avanzarAFase(FaseJuego.votacion),
|
|
);
|
|
case FaseJuego.votacion:
|
|
return BotonFarolero(
|
|
texto: todosVotaron ? l10n.revealResult : l10n.waitingVoting,
|
|
icono: Icons.visibility,
|
|
assetIconPath: 'assets/ui/generated/actions/action_result_trophy.webp',
|
|
onPressed: todosVotaron ? () => _avanzarAFase(FaseJuego.resultado) : null,
|
|
);
|
|
case FaseJuego.resultado:
|
|
return _buildAccionesResultado(context, l10n);
|
|
case FaseJuego.adivinanza:
|
|
return _buildAccionesAdivinanza(context, l10n);
|
|
case FaseJuego.finPartida:
|
|
return BotonFarolero.oscuro(
|
|
texto: l10n.mainMenu,
|
|
icono: Icons.home,
|
|
onPressed: () async {
|
|
final nearby = context.read<ServicioNearby>();
|
|
await nearby.desconectar();
|
|
widget.onPartidaFin();
|
|
},
|
|
);
|
|
default:
|
|
return const SizedBox.shrink();
|
|
}
|
|
}
|
|
|
|
Widget _buildAccionesResultado(BuildContext context, AppLocalizations l10n) {
|
|
final estado = context.read<EstadoJuego>();
|
|
final partida = estado.partida;
|
|
final resultado = partida?.historialVotaciones.isNotEmpty == true
|
|
? partida!.historialVotaciones.last
|
|
: null;
|
|
if (partida == null || resultado == null) return const SizedBox.shrink();
|
|
|
|
if (_hayFinTrasVotacion(partida)) {
|
|
return BotonFarolero(
|
|
texto: l10n.seeEndResult,
|
|
icono: Icons.emoji_events,
|
|
assetIconPath: 'assets/ui/generated/actions/action_result_trophy.webp',
|
|
onPressed: () => _finalizarPartidaOnline(context),
|
|
);
|
|
}
|
|
|
|
if (resultado.eraImpostor) {
|
|
return Column(
|
|
children: [
|
|
BotonFarolero.oscuro(
|
|
texto: l10n.impostorGuessWord,
|
|
icono: Icons.psychology,
|
|
assetIconPath: 'assets/ui/generated/actions/action_impostor_mask.webp',
|
|
onPressed: () => _iniciarAdivinanzaOnline(context),
|
|
),
|
|
const SizedBox(height: 12),
|
|
BotonFarolero(
|
|
texto: l10n.nextRound,
|
|
icono: Icons.skip_next,
|
|
onPressed: () => _siguienteRondaOnline(context),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
return BotonFarolero(
|
|
texto: l10n.nextRound,
|
|
icono: Icons.skip_next,
|
|
onPressed: () => _siguienteRondaOnline(context),
|
|
);
|
|
}
|
|
|
|
Widget _buildAccionesAdivinanza(BuildContext context, AppLocalizations l10n) {
|
|
return Column(
|
|
children: [
|
|
BotonFarolero(
|
|
texto: l10n.guess,
|
|
icono: Icons.check_circle,
|
|
assetIconPath: 'assets/ui/generated/actions/action_impostor_mask.webp',
|
|
onPressed: () => _resolverAdivinanzaOnline(context),
|
|
),
|
|
const SizedBox(height: 12),
|
|
BotonFarolero.oscuro(
|
|
texto: l10n.dontGuess,
|
|
icono: Icons.skip_next,
|
|
onPressed: () => _siguienteRondaOnline(context),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Future<void> _finalizarPartidaOnline(BuildContext context) async {
|
|
final estado = context.read<EstadoJuego>();
|
|
final nearby = context.read<ServicioNearby>();
|
|
estado.comprobarFinPartida();
|
|
final snapshotFinal = _snapshot(fase: 'finPartida', revelarFinal: true)
|
|
.toJson();
|
|
await _guardarHistorialOnlineHost(context);
|
|
await nearby.enviarFinPartida(snapshotFinal);
|
|
if (mounted) setState(() {});
|
|
}
|
|
|
|
Future<void> _guardarHistorialOnlineHost(BuildContext context) async {
|
|
if (_partidaOnlineGuardada) return;
|
|
final partida = context.read<EstadoJuego>().partida;
|
|
if (partida?.ganador == null) return;
|
|
_partidaOnlineGuardada = true;
|
|
final historial = context.read<ServicioHistorialPartidas>();
|
|
final nearby = context.read<ServicioNearby>();
|
|
final perfil = context.read<ServicioPerfilUsuario>();
|
|
await historial.guardarPartida(partida!);
|
|
final jugadoresHost = _jugadoresHostControlados(
|
|
partida,
|
|
nearby,
|
|
);
|
|
final ganaronImpostores = partida.ganador == 'impostores';
|
|
final victoria = jugadoresHost.isEmpty
|
|
? partida.ganador == 'jugadores'
|
|
: jugadoresHost.any(
|
|
(jugador) =>
|
|
jugador.esImpostor ? ganaronImpostores : !ganaronImpostores,
|
|
);
|
|
final progreso = await perfil.registrarPartidaCompletada(
|
|
victoria: victoria,
|
|
comoImpostor: jugadoresHost.any((j) => j.esImpostor),
|
|
victoriaComoImpostor:
|
|
ganaronImpostores && jugadoresHost.any((j) => j.esImpostor),
|
|
);
|
|
if (mounted) setState(() => _progresoGamificacion = progreso);
|
|
}
|
|
|
|
Future<void> _iniciarAdivinanzaOnline(BuildContext context) async {
|
|
final estado = context.read<EstadoJuego>();
|
|
final nearby = context.read<ServicioNearby>();
|
|
estado.iniciarAdivinanza();
|
|
await nearby.enviarCambioFase(
|
|
'adivinanza',
|
|
_snapshot(
|
|
fase: 'adivinanza',
|
|
mensaje: AppLocalizations.of(context)!.impostorCanGuess,
|
|
).toJson(),
|
|
);
|
|
if (mounted) setState(() {});
|
|
}
|
|
|
|
Future<void> _resolverAdivinanzaOnline(BuildContext context) async {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final controller = TextEditingController();
|
|
final intento = await showDialog<String>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: Text(l10n.impostorGuessTitle),
|
|
content: TextField(
|
|
controller: controller,
|
|
autofocus: true,
|
|
decoration: InputDecoration(hintText: l10n.guessWordHint),
|
|
onSubmitted: (value) => Navigator.pop(ctx, value),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: Text(l10n.cancel),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, controller.text),
|
|
child: Text(l10n.accept),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
controller.dispose();
|
|
if (!context.mounted) return;
|
|
if (intento == null || intento.trim().isEmpty) {
|
|
await _siguienteRondaOnline(context);
|
|
return;
|
|
}
|
|
final estado = context.read<EstadoJuego>();
|
|
final acierto = estado.intentarAdivinar(intento);
|
|
if (acierto) {
|
|
final nearby = context.read<ServicioNearby>();
|
|
final snapshotFinal = _snapshot(fase: 'finPartida', revelarFinal: true)
|
|
.toJson();
|
|
await _guardarHistorialOnlineHost(context);
|
|
await nearby.enviarFinPartida(snapshotFinal);
|
|
if (mounted) setState(() {});
|
|
return;
|
|
}
|
|
await _siguienteRondaOnline(context);
|
|
}
|
|
|
|
Future<void> _siguienteRondaOnline(BuildContext context) async {
|
|
final estado = context.read<EstadoJuego>();
|
|
final nearby = context.read<ServicioNearby>();
|
|
estado.siguienteRonda();
|
|
_primerTurnoId = null;
|
|
_primerTurnoNombre = null;
|
|
final primero = _elegirPrimerTurno();
|
|
await nearby.enviarCambioFase('debate', {
|
|
..._snapshot(fase: 'debate').toJson(),
|
|
if (primero != null) ...{
|
|
'primerTurnoId': primero.id,
|
|
'primerTurnoNombre': primero.nombre,
|
|
},
|
|
if (estado.partida?.config.tiempoDebateSegundos != null)
|
|
'tiempoDebateSegundos': estado.partida!.config.tiempoDebateSegundos,
|
|
});
|
|
_timer?.cancel();
|
|
_iniciarTemporizador();
|
|
if (mounted) setState(() {});
|
|
}
|
|
}
|
|
|
|
class _PantallaRevelarPalabraHost extends StatefulWidget {
|
|
final String nombre;
|
|
final bool esImpostor;
|
|
final String palabra;
|
|
final bool pistaActiva;
|
|
final String pista;
|
|
final List<String>? companerosImpostores;
|
|
final VoidCallback onVisto;
|
|
|
|
const _PantallaRevelarPalabraHost({
|
|
required this.nombre,
|
|
required this.esImpostor,
|
|
required this.palabra,
|
|
required this.pistaActiva,
|
|
required this.pista,
|
|
required this.companerosImpostores,
|
|
required this.onVisto,
|
|
});
|
|
|
|
@override
|
|
State<_PantallaRevelarPalabraHost> createState() =>
|
|
_PantallaRevelarPalabraHostState();
|
|
}
|
|
|
|
class _PantallaRevelarPalabraHostState
|
|
extends State<_PantallaRevelarPalabraHost> {
|
|
bool _manteniendo = false;
|
|
bool _haRevelado = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(widget.nombre)),
|
|
body: FondoFarolero(
|
|
intenso: true,
|
|
child: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
widget.nombre,
|
|
style: Theme.of(context).textTheme.headlineMedium,
|
|
),
|
|
const SizedBox(height: 32),
|
|
AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(32),
|
|
decoration: BoxDecoration(
|
|
color: _manteniendo
|
|
? (widget.esImpostor
|
|
? TemaApp.colorAcento.withValues(alpha: 0.3)
|
|
: TemaApp.colorVerde.withValues(alpha: 0.3))
|
|
: TemaApp.colorTarjeta,
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: _manteniendo
|
|
? (widget.esImpostor
|
|
? TemaApp.colorAcento
|
|
: TemaApp.colorVerde)
|
|
: Colors.transparent,
|
|
width: 2,
|
|
),
|
|
),
|
|
child: _manteniendo
|
|
? Column(
|
|
children: [
|
|
IconoFarolero(
|
|
widget.esImpostor
|
|
? Icons.theater_comedy
|
|
: Icons.key,
|
|
color: widget.esImpostor
|
|
? TemaApp.colorAcento
|
|
: TemaApp.colorVerde,
|
|
size: 48,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
widget.esImpostor
|
|
? l10n.youAreImpostor
|
|
: l10n.yourWordIs,
|
|
style: Theme.of(context).textTheme.titleLarge
|
|
?.copyWith(
|
|
color: widget.esImpostor
|
|
? TemaApp.colorAcento
|
|
: TemaApp.colorVerde,
|
|
),
|
|
),
|
|
if (!widget.esImpostor) ...[
|
|
const SizedBox(height: 12),
|
|
TarjetaPalabraFarolero(palabra: widget.palabra),
|
|
],
|
|
if (widget.esImpostor && widget.pistaActiva) ...[
|
|
const SizedBox(height: 12),
|
|
PistaImpostorFarolero(pista: widget.pista),
|
|
],
|
|
if (widget.esImpostor &&
|
|
widget.companerosImpostores != null) ...[
|
|
const SizedBox(height: 12),
|
|
CompanerosImpostorFarolero(
|
|
nombres: widget.companerosImpostores!,
|
|
),
|
|
],
|
|
],
|
|
)
|
|
: Column(
|
|
children: [
|
|
IconoFarolero(
|
|
Icons.lock,
|
|
color: TemaApp.colorDorado,
|
|
size: 48,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
l10n.holdToSeeWord,
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
l10n.makeSureNoOneLooks,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
GestureDetector(
|
|
onLongPressStart: (_) => setState(() {
|
|
_manteniendo = true;
|
|
_haRevelado = true;
|
|
}),
|
|
onLongPressEnd: (_) => setState(() => _manteniendo = false),
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: 64,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: _manteniendo
|
|
? [TemaApp.colorNaranja, TemaApp.colorAcento]
|
|
: [TemaApp.colorAcento, TemaApp.colorAcento],
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
_manteniendo ? l10n.showingWord : l10n.holdToSee,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
BotonFarolero(
|
|
texto: _haRevelado ? l10n.iveSeenIt : l10n.tapToSee,
|
|
icono: Icons.check,
|
|
onPressed: _haRevelado
|
|
? () {
|
|
widget.onVisto();
|
|
Navigator.of(context).pop();
|
|
}
|
|
: null,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|