Files
farolero/lib/pantallas/pantalla_votacion_cliente.dart
T
FreeTLab ae0d55c00f
Build & Deploy Farolero / Análisis de código (push) Successful in 14s
Build & Deploy Farolero / Build APK + AAB release (push) Successful in 1m16s
fix(android): target API level 35 as required by Google Play
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.
2026-07-25 22:27:28 +02:00

230 lines
7.8 KiB
Dart

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/pantallas/pantalla_notas_online.dart';
import 'package:farolero/pantallas/pantalla_revision_palabra.dart';
import 'package:farolero/tema/componentes_farolero.dart';
import 'package:farolero/tema/tema_app.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<Jugador> jugadores;
final List<JugadorInicioPartida> jugadoresControlados;
final String? partidaId;
final String? pistaImpostor;
final Function(Map<String, String> votos) onVotos;
const PantallaVotacionCliente({
super.key,
required this.jugadores,
this.jugadoresControlados = const [],
this.partidaId,
this.pistaImpostor,
required this.onVotos,
});
@override
State<PantallaVotacionCliente> createState() => _PantallaVotacionClienteState();
}
class _PantallaVotacionClienteState extends State<PantallaVotacionCliente> {
final Map<String, String> _votosPorVotante = {};
/// Solo los jugadores vivos pueden ser votados.
List<Jugador> get _votables =>
widget.jugadores.where((jugador) => !jugador.eliminado).toList();
/// Y solo los jugadores vivos que controla este dispositivo pueden votar.
List<JugadorInicioPartida> get _votantes => widget.jugadoresControlados
.where(
(controlado) =>
_votables.any((jugador) => jugador.id == controlado.jugadorId),
)
.toList();
/// Protocolo antiguo: el cliente no recibió jugadores controlados y emite un
/// único voto sin identificar al votante.
bool get _modoLegacy => widget.jugadoresControlados.isEmpty;
/// Todos los jugadores de este dispositivo están eliminados: no vota nadie.
bool get _sinVotantesVivos => !_modoLegacy && _votantes.isEmpty;
bool get _votacionCompleta {
if (_sinVotantesVivos) return false;
if (_modoLegacy) return _votosPorVotante.containsKey('_legacy');
return _votantes.every(
(votante) => _votosPorVotante[votante.jugadorId] != null,
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
backgroundColor: TemaApp.colorFondo,
appBar: AppBar(
title: Text(l10n.voting),
automaticallyImplyLeading: false,
backgroundColor: Colors.transparent,
elevation: 0,
actions: [
IconButton(
tooltip: l10n.seeYourWord,
icon: IconoFarolero(Icons.visibility),
onPressed: widget.jugadoresControlados.isEmpty
? null
: () => mostrarRevisionPalabraOnline(
context: context,
jugadoresControlados: widget.jugadoresControlados,
pistaImpostor: widget.pistaImpostor,
),
),
IconButton(
tooltip: l10n.notesTitle,
icon: IconoFarolero(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,
respetarBarras: true,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ArteGameplayFarolero.fase(height: 108),
const SizedBox(height: 10),
EncabezadoFarolero(
icono: Icons.how_to_vote,
titulo: l10n.whoDoYouThinkIsTheImpostor,
subtitulo: l10n.selectOnePlayer,
color: TemaApp.colorAcento,
trailing: Image.asset(
'assets/ui/generated/meta/result_verdict_art.webp',
width: 42,
height: 42,
opacity: const AlwaysStoppedAnimation(0.64),
),
),
const SizedBox(height: 16),
Expanded(
child: _sinVotantesVivos
? Center(
child: EstadoVacioFarolero(
icono: Icons.hourglass_empty,
titulo: l10n.eliminatedCannotVote,
subtitulo: l10n.waitingVoting,
),
)
: _modoLegacy
? _buildSelectorLegacy()
: ListView.builder(
itemCount: _votantes.length,
itemBuilder: (context, index) {
final votante = _votantes[index];
return _buildSelectorParaVotante(context, votante);
},
),
),
const SizedBox(height: 16),
BotonFarolero.secundario(
texto: l10n.votar,
icono: Icons.how_to_vote,
assetIconPath: 'assets/ui/generated/actions/action_vote_mask.webp',
onPressed: _votacionCompleta
? () => widget.onVotos(Map.unmodifiable(_votosPorVotante))
: null,
),
],
),
),
),
);
}
bool get _puedeAbrirNotas {
return widget.partidaId != null &&
_votables.isNotEmpty &&
widget.jugadoresControlados.isNotEmpty;
}
Widget _buildSelectorLegacy() {
final votables = _votables;
return ListView.builder(
itemCount: votables.length,
itemBuilder: (context, index) {
final jugador = votables[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 PanelFarolero(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(14),
borderColor: TemaApp.colorAcento.withValues(alpha: 0.48),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.voteOf(votante.nombre),
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
..._votables.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 SelectorVotoFarolero(
nombre: '${index + 1}. ${jugador.nombre}',
seleccionado: selected,
onTap: onTap,
);
}
}