Exit confirmation - A dialog in the app's own style now guards every in-game screen. The safe action is the prominent one, so a stray tap keeps you in the game. - Eleven screens covered, against both the system back gesture and the close buttons. The message adapts: a single-device game is lost, a host ends it for everyone, a client can rejoin. - The two screens the host also opens from the manager take a flag so that back there just closes them instead of offering to quit. Translations - 21 keys were still holding the English text in Arabic, Basque, Hindi, Japanese, Korean, Dutch, Polish, Russian, Turkish and both Chinese variants: 231 strings, covering the whole multi-device flow, permissions and the mode picker. Nothing failed, the app just showed English. - The permissions dialog was hardcoded in Spanish; it is localized now. - Portuguese used Spanish gerunds where pt-PT takes "a + infinitive". Checked what looked untranslated but is not: Animals and Notes are correct Catalan, Configuration and Vibration correct French, Version correct German. Those are whitelisted in the new test rather than silently rewritten. l10n_cobertura_test guards all of it: no missing or orphan keys, no empty values, no English left outside the whitelist, and placeholders preserved.
1318 lines
45 KiB
Dart
1318 lines
45 KiB
Dart
import 'dart:async';
|
|
import '../tema/dialogo_confirmacion.dart';
|
|
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 GuardiaSalidaPartida(
|
|
contexto: ContextoSalida.host,
|
|
child: 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(
|
|
tooltip: l10n.leaveGame,
|
|
icon: IconoFarolero(Icons.close),
|
|
onPressed: () async {
|
|
if (!await confirmarSalirDePartida(
|
|
context,
|
|
ContextoSalida.host,
|
|
)) {
|
|
return;
|
|
}
|
|
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(
|
|
protegerSalida: false,
|
|
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(
|
|
protegerSalida: false,
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|