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.
230 lines
7.8 KiB
Dart
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,
|
|
);
|
|
}
|
|
}
|