import 'package:flutter/material.dart'; import 'package:farolero/l10n/generated/app_localizations.dart'; import 'package:farolero/modelos/inicio_partida_multijugador.dart'; import 'package:farolero/modelos/jugador.dart'; import 'package:farolero/modelos/snapshot_partida_online.dart'; import 'package:farolero/pantallas/pantalla_notas_online.dart'; import 'package:farolero/pantallas/pantalla_revision_palabra.dart'; import 'package:farolero/pantallas/pantalla_resultado_online.dart'; import 'package:farolero/servicios/servicio_nearby.dart'; import 'package:farolero/tema/componentes_farolero.dart'; import 'package:farolero/tema/tema_app.dart'; import 'package:provider/provider.dart'; /// Pantalla de votación para cliente multidispositivo. /// Un cliente puede manejar uno o varios jugadores, por eso se recoge un voto /// por cada jugador controlado activo. class PantallaVotacionCliente extends StatefulWidget { final List jugadores; final List jugadoresControlados; final String? partidaId; final String? pistaCategoria; final Function(Map votos) onVotos; const PantallaVotacionCliente({ super.key, required this.jugadores, this.jugadoresControlados = const [], this.partidaId, this.pistaCategoria, required this.onVotos, }); @override State createState() => _PantallaVotacionClienteState(); } class _PantallaVotacionClienteState extends State { final Map _votosPorVotante = {}; Map? _resultado; OnMensajeCallback? _listener; ServicioNearby? _nearby; List get _votantes => widget.jugadoresControlados; bool get _modoMultiVotante => _votantes.length > 1; bool get _votacionCompleta { if (_votantes.isEmpty) return _votosPorVotante.containsKey('_legacy'); return _votantes.every((votante) => _votosPorVotante[votante.jugadorId] != null); } @override void initState() { super.initState(); _listener = (endpointId, mensaje) { if (mensaje.tipo != TipoMensaje.votacionResultado || !mounted) return; if (mensaje.datos.containsKey('jugadoresTodos')) { final snapshot = SnapshotPartidaOnline.fromJson(mensaje.datos); Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (_) => PantallaResultadoOnline( snapshot: snapshot, jugadoresControlados: widget.jugadoresControlados, pistaCategoria: widget.pistaCategoria, ), ), ); } else { setState(() => _resultado = mensaje.datos); } }; WidgetsBinding.instance.addPostFrameCallback((_) { final listener = _listener; if (listener != null && mounted) { _nearby = context.read(); _nearby!.onMensaje(listener); } }); } @override void dispose() { final listener = _listener; if (listener != null) { _nearby?.removeMensajeListener(listener); } super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; if (_resultado != null) return _buildResultado(context, _resultado!); return Scaffold( backgroundColor: TemaApp.colorFondo, appBar: AppBar( title: Text(l10n.voting), automaticallyImplyLeading: false, backgroundColor: Colors.transparent, elevation: 0, actions: [ IconButton( tooltip: l10n.seeYourWord, icon: const Icon(Icons.visibility), onPressed: widget.jugadoresControlados.isEmpty ? null : () => mostrarRevisionPalabraOnline( context: context, jugadoresControlados: widget.jugadoresControlados, pistaCategoria: widget.pistaCategoria, ), ), IconButton( tooltip: l10n.notesTitle, icon: const Icon(Icons.edit_note), onPressed: _puedeAbrirNotas ? () => Navigator.push( context, MaterialPageRoute( builder: (_) => PantallaNotasOnline( partidaId: widget.partidaId!, jugadores: widget.jugadores, autoresControlados: widget.jugadoresControlados, ), ), ) : null, ), ], ), body: FondoFarolero( intenso: true, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ EncabezadoFarolero( icono: Icons.how_to_vote, titulo: l10n.whoDoYouThinkIsTheImpostor, subtitulo: _modoMultiVotante ? 'Emití un voto por cada jugador que manejás.' : l10n.selectOnePlayer, color: TemaApp.colorAcento, trailing: Image.asset( 'assets/ui/premium/vote_danger_glow.png', width: 42, height: 42, opacity: const AlwaysStoppedAnimation(0.64), ), ), const SizedBox(height: 16), Expanded( child: _votantes.isEmpty ? _buildSelectorLegacy() : ListView.builder( itemCount: _votantes.length, itemBuilder: (context, index) { final votante = _votantes[index]; return _buildSelectorParaVotante(context, votante); }, ), ), const SizedBox(height: 16), SizedBox( width: double.infinity, height: 56, child: ElevatedButton.icon( onPressed: _votacionCompleta ? () => widget.onVotos(Map.unmodifiable(_votosPorVotante)) : null, icon: const Icon(Icons.how_to_vote), label: Text(l10n.votar), style: ElevatedButton.styleFrom( backgroundColor: TemaApp.colorAcento, foregroundColor: Colors.white, textStyle: const TextStyle(fontSize: 16), ), ), ), ], ), ), ), ); } bool get _puedeAbrirNotas { return widget.partidaId != null && widget.jugadores.isNotEmpty && widget.jugadoresControlados.isNotEmpty; } Widget _buildResultado(BuildContext context, Map resultado) { final l10n = AppLocalizations.of(context)!; final eliminadoId = resultado['eliminadoId'] as String?; final eliminadoNombre = resultado['eliminadoNombre'] as String? ?? '?'; final eraImpostor = resultado['eraImpostor'] as bool? ?? false; final votosRaw = resultado['votos'] as Map? ?? {}; final votos = votosRaw.map( (key, value) => MapEntry(key.toString(), value.toString()), ); final jugadores = {for (final jugador in widget.jugadores) jugador.id: jugador}; final conteo = {}; for (final votadoId in votos.values) { conteo[votadoId] = (conteo[votadoId] ?? 0) + 1; } final maxVotos = conteo.values.isEmpty ? 1 : conteo.values.reduce((a, b) => a > b ? a : b); final ranking = conteo.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); return Scaffold( backgroundColor: TemaApp.colorFondo, appBar: AppBar( title: const Text('Resultado'), automaticallyImplyLeading: false, backgroundColor: Colors.transparent, elevation: 0, actions: [ IconButton( tooltip: l10n.seeYourWord, icon: const Icon(Icons.visibility), onPressed: widget.jugadoresControlados.isEmpty ? null : () => mostrarRevisionPalabraOnline( context: context, jugadoresControlados: widget.jugadoresControlados, pistaCategoria: widget.pistaCategoria, ), ), IconButton( tooltip: l10n.notesTitle, icon: const Icon(Icons.edit_note), onPressed: _puedeAbrirNotas ? () => Navigator.push( context, MaterialPageRoute( builder: (_) => PantallaNotasOnline( partidaId: widget.partidaId!, jugadores: widget.jugadores, autoresControlados: widget.jugadoresControlados, ), ), ) : null, ), ], ), body: FondoFarolero( intenso: true, child: Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: eraImpostor ? TemaApp.colorVerde.withValues(alpha: 0.18) : TemaApp.colorAcento.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(18), border: Border.all( color: eraImpostor ? TemaApp.colorVerde : TemaApp.colorAcento, ), ), child: Column( children: [ Text( eliminadoNombre, style: Theme.of(context).textTheme.headlineMedium, textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( eraImpostor ? 'Era impostor' : 'Era inocente', style: TextStyle( color: eraImpostor ? TemaApp.colorVerde : TemaApp.colorAcento, fontWeight: FontWeight.bold, ), ), ], ), ), const SizedBox(height: 20), Text( 'Detalle de votos', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 12), Expanded( child: ListView( children: [ ...ranking.map((entry) { final jugador = jugadores[entry.key]; final destacado = entry.key == eliminadoId; final color = destacado ? TemaApp.colorAcento : TemaApp.colorNaranja; return Padding( padding: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded(child: Text(jugador?.nombre ?? '?')), Text( '${entry.value}', style: TextStyle( color: color, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 6), ClipRRect( borderRadius: BorderRadius.circular(999), child: LinearProgressIndicator( value: (entry.value / maxVotos) .clamp(0.0, 1.0) .toDouble(), minHeight: 10, backgroundColor: TemaApp.colorSuperficie, valueColor: AlwaysStoppedAnimation(color), ), ), ], ), ); }), const Divider(height: 24), ...votos.entries.map((entry) { final votante = jugadores[entry.key]?.nombre ?? '?'; final votado = jugadores[entry.value]?.nombre ?? '?'; return ListTile( dense: true, leading: const Icon(Icons.how_to_vote), title: Text('$votante → $votado'), ); }), ], ), ), ], ), ), ), ); } Widget _buildSelectorLegacy() { return ListView.builder( itemCount: widget.jugadores.length, itemBuilder: (context, index) { final jugador = widget.jugadores[index]; final selected = _votosPorVotante['_legacy'] == jugador.id; return _buildJugadorVotable( jugador: jugador, index: index, selected: selected, onTap: () => setState(() => _votosPorVotante['_legacy'] = jugador.id), ); }, ); } Widget _buildSelectorParaVotante( BuildContext context, JugadorInicioPartida votante, ) { return Card( color: TemaApp.colorSuperficie, margin: const EdgeInsets.only(bottom: 16), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Voto de ${votante.nombre}', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), ...widget.jugadores.asMap().entries.map((entry) { final jugador = entry.value; final selected = _votosPorVotante[votante.jugadorId] == jugador.id; return _buildJugadorVotable( jugador: jugador, index: entry.key, selected: selected, onTap: () => setState( () => _votosPorVotante[votante.jugadorId] = jugador.id, ), ); }), ], ), ), ); } Widget _buildJugadorVotable({ required Jugador jugador, required int index, required bool selected, required VoidCallback onTap, }) { return Card( color: selected ? TemaApp.colorAcento.withValues(alpha: 0.3) : TemaApp.colorTarjeta, margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: CircleAvatar( backgroundColor: selected ? TemaApp.colorAcento : TemaApp.colorAcento.withValues(alpha: 0.3), child: Text( '${index + 1}', style: TextStyle( color: selected ? Colors.white : TemaApp.colorTexto, ), ), ), title: Text(jugador.nombre), trailing: selected ? const Icon(Icons.check_circle, color: TemaApp.colorAcento) : null, onTap: onTap, ), ); } }