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.
736 lines
26 KiB
Dart
736 lines
26 KiB
Dart
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/inicio_partida_multijugador.dart';
|
|
import '../modelos/palabra.dart';
|
|
import '../modelos/partida.dart';
|
|
import '../servicios/servicio_nearby.dart';
|
|
import '../servicios/servicio_permisos.dart';
|
|
import '../servicios/servicio_perfil_usuario.dart';
|
|
import '../tema/componentes_farolero.dart';
|
|
import '../tema/tema_app.dart';
|
|
import 'pantalla_gestor_host.dart';
|
|
import 'pantalla_lobby_host.dart';
|
|
import 'pantalla_principal.dart';
|
|
import 'pantalla_ver_palabra.dart';
|
|
|
|
class PantallaCrearPartida extends StatefulWidget {
|
|
final bool modoInicial;
|
|
final bool bloquearModo;
|
|
|
|
const PantallaCrearPartida({
|
|
super.key,
|
|
this.modoInicial = false,
|
|
this.bloquearModo = false,
|
|
});
|
|
|
|
@override
|
|
State<PantallaCrearPartida> createState() => _PantallaCrearPartidaState();
|
|
}
|
|
|
|
class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
|
bool _modoMultimovil = false;
|
|
String _categoria = 'todas';
|
|
int _numImpostores = 1;
|
|
bool _pistaImpostor = false;
|
|
bool _impostoresSeConocen = true;
|
|
int? _tiempoDebate;
|
|
final List<String> _jugadores = [];
|
|
final _controladorNombre = TextEditingController();
|
|
bool _agregandoPerfilPendiente = false;
|
|
|
|
final _opcionesTiempo = <int?>[null, 60, 120, 180, 300];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_modoMultimovil = widget.modoInicial;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) _agregarPerfilLocalSiNecesario();
|
|
});
|
|
}
|
|
|
|
void _agregarPerfilLocalSiNecesario() {
|
|
if (_modoMultimovil) return;
|
|
final servicioPerfil = context.read<ServicioPerfilUsuario>();
|
|
if (!servicioPerfil.cargado) return;
|
|
final perfil = servicioPerfil.perfil;
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final nombre = perfil.nombre.trim().isEmpty
|
|
? l10n.defaultPlayerName
|
|
: perfil.nombre.trim();
|
|
if (_jugadores.contains(nombre)) return;
|
|
setState(() {
|
|
_jugadores.insert(0, nombre);
|
|
});
|
|
}
|
|
|
|
/// En multidispositivo el número real de jugadores se conoce en el lobby, así
|
|
/// que aquí se permite el tope global; al iniciar se ajusta y se avisa.
|
|
int get _maxImpostores => _modoMultimovil
|
|
? 4
|
|
: EstadoJuego.maxImpostoresPara(_jugadores.length);
|
|
|
|
List<String> _etiquetasTiempo(AppLocalizations l10n) => [
|
|
l10n.noLimit,
|
|
l10n.oneMin,
|
|
l10n.twoMin,
|
|
l10n.threeMin,
|
|
l10n.fiveMin,
|
|
];
|
|
|
|
void _agregarJugador() {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final nombre = _controladorNombre.text.trim();
|
|
if (nombre.isEmpty) return;
|
|
if (_jugadores.contains(nombre)) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(l10n.playerAlreadyExists)));
|
|
return;
|
|
}
|
|
if (_jugadores.length >= 20) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(l10n.maxPlayersReached)));
|
|
return;
|
|
}
|
|
setState(() {
|
|
_jugadores.add(nombre);
|
|
_controladorNombre.clear();
|
|
if (_numImpostores > _maxImpostores) {
|
|
_numImpostores = _maxImpostores;
|
|
}
|
|
});
|
|
}
|
|
|
|
void _eliminarJugador(int index) {
|
|
final perfil = context.read<ServicioPerfilUsuario>().perfil;
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final nombrePerfil = perfil.nombre.trim().isEmpty
|
|
? l10n.defaultPlayerName
|
|
: perfil.nombre.trim();
|
|
if (index == 0 && _jugadores[index] == nombrePerfil) return;
|
|
setState(() {
|
|
_jugadores.removeAt(index);
|
|
if (_numImpostores > _maxImpostores && _maxImpostores > 0) {
|
|
_numImpostores = _maxImpostores;
|
|
}
|
|
});
|
|
}
|
|
|
|
void _iniciarPartida() {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
|
|
if (_modoMultimovil) {
|
|
_iniciarPartidaMulti();
|
|
return;
|
|
}
|
|
|
|
if (_jugadores.length < 3) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(l10n.minPlayersRequired)));
|
|
return;
|
|
}
|
|
|
|
final estado = context.read<EstadoJuego>();
|
|
estado.crearPartida(
|
|
config: ConfigPartida(
|
|
modoMultimovil: false,
|
|
categoria: _categoria,
|
|
numImpostores: _numImpostores,
|
|
pistaImpostor: _pistaImpostor,
|
|
tiempoDebateSegundos: _tiempoDebate,
|
|
impostoresSeConocen: _impostoresSeConocen,
|
|
),
|
|
nombresJugadores: _jugadores,
|
|
);
|
|
|
|
Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const PantallaVerPalabra()),
|
|
);
|
|
}
|
|
|
|
Future<void> _iniciarPartidaMulti() async {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
// 1. Pedir permisos automáticamente
|
|
final permisosOk = await ServicioPermisos.solicitarPermisosNearby(context);
|
|
if (!permisosOk) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(l10n.bluetoothLocationPermissionsShort),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 2. Usar el perfil principal del dispositivo como usuario del host.
|
|
final nombre = await _seleccionarUsuarioHost();
|
|
if (nombre == null || nombre.trim().isEmpty) return;
|
|
|
|
// 3. Iniciar host en Nearby
|
|
if (!mounted) return;
|
|
final nearby = context.read<ServicioNearby>();
|
|
final servicioPerfil = context.read<ServicioPerfilUsuario>();
|
|
final perfil = servicioPerfil.perfil;
|
|
final gamificacion = servicioPerfil.resumenGamificacion;
|
|
final nombreSala = '${nombre.trim()} - Farolero';
|
|
final ok = await nearby.iniciarHost(
|
|
nombreSala,
|
|
nombre.trim(),
|
|
miNick: perfil.nick,
|
|
miAvatar: perfil.avatarAsset,
|
|
miFuego: gamificacion.fuego,
|
|
miMedallas: gamificacion.medallas,
|
|
);
|
|
|
|
if (!ok) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(l10n.couldNotCreateRoom),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 4. Navegar al lobby con QR
|
|
if (mounted) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => PantallaLobbyHost(
|
|
nombreSala: nombreSala,
|
|
onIniciar: () {
|
|
// Cuando el host toca "Iniciar" con suficientes jugadores
|
|
final estado = context.read<EstadoJuego>();
|
|
final sala = nearby.estadoSala;
|
|
if (sala == null) return;
|
|
final validacion = sala.iniciarPartida();
|
|
if (!validacion.exitoso) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
l10n.cannotStartWithReason(
|
|
validacion.codigo ?? l10n.invalidRoom,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
estado.crearPartidaDesdeSala(
|
|
config: ConfigPartida(
|
|
modoMultimovil: true,
|
|
categoria: _categoria,
|
|
numImpostores: _numImpostores,
|
|
pistaImpostor: _pistaImpostor,
|
|
tiempoDebateSegundos: _tiempoDebate,
|
|
impostoresSeConocen: _impostoresSeConocen,
|
|
),
|
|
sala: sala,
|
|
);
|
|
|
|
final partida = estado.partida!;
|
|
// El tope de impostores depende de cuántos jugadores hay, y eso
|
|
// solo se sabe aquí. Si se recorta, hay que decirlo.
|
|
if (partida.impostoresTotales < _numImpostores) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
l10n.impostorsAdjusted(partida.impostoresTotales),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
final asignaciones = partida.jugadores.map((jugador) {
|
|
final usuarioSala = sala.usuarios[jugador.id];
|
|
final clientId = usuarioSala?.clienteIdSeleccionado;
|
|
final cliente = clientId == null ? null : sala.clientes[clientId];
|
|
return AsignacionJugador(
|
|
jugadorId: jugador.id,
|
|
nombre: jugador.nombre,
|
|
clientId: clientId ?? sala.hostClientId,
|
|
endpointId: cliente?.endpointId,
|
|
);
|
|
}).toList();
|
|
final impostores = {
|
|
for (final jugador in partida.jugadores)
|
|
jugador.id: jugador.esImpostor,
|
|
};
|
|
final jugadoresTodos = partida.jugadores
|
|
.map(
|
|
(jugador) => {
|
|
'id': jugador.id,
|
|
'nombre': jugador.nombre,
|
|
'eliminado': jugador.eliminado,
|
|
},
|
|
)
|
|
.toList();
|
|
|
|
nearby.enviarInicioPartidaMulti(
|
|
asignaciones: asignaciones,
|
|
palabraSecreta: partida.palabraSecreta,
|
|
categoria: partida.categoriaReal,
|
|
impostoresPorJugadorId: impostores,
|
|
jugadoresTodos: jugadoresTodos,
|
|
impostoresSeConocen: _impostoresSeConocen,
|
|
// La pista solo sale del host si la partida la tiene activada.
|
|
pistaImpostor: _pistaImpostor ? partida.pistaImpostor : null,
|
|
);
|
|
|
|
Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => PantallaGestorHost(
|
|
onPartidaFin: () {
|
|
estado.limpiar();
|
|
Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => const PantallaPrincipal(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Devuelve el perfil principal del dispositivo para crear la sala.
|
|
Future<String?> _seleccionarUsuarioHost() async {
|
|
return context.read<ServicioPerfilUsuario>().perfil.nombre;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controladorNombre.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final estado = context.watch<EstadoJuego>();
|
|
final servicioPerfil = context.watch<ServicioPerfilUsuario>();
|
|
final categorias = ['todas', ...?estado.banco?.nombresCategorias];
|
|
final etiquetas = _etiquetasTiempo(l10n);
|
|
final nombrePerfilActual = servicioPerfil.perfil.nombre.trim().isEmpty
|
|
? l10n.defaultPlayerName
|
|
: servicioPerfil.perfil.nombre.trim();
|
|
if (!_modoMultimovil &&
|
|
servicioPerfil.cargado &&
|
|
!_agregandoPerfilPendiente &&
|
|
!_jugadores.contains(nombrePerfilActual)) {
|
|
_agregandoPerfilPendiente = true;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_agregandoPerfilPendiente = false;
|
|
if (mounted) _agregarPerfilLocalSiNecesario();
|
|
});
|
|
}
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(l10n.createGame)),
|
|
body: FondoFarolero(
|
|
intenso: true,
|
|
respetarBarras: true,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.fromLTRB(18, 18, 18, 28),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_CrearPartidaHeader(
|
|
titulo: l10n.createGame,
|
|
subtitulo: l10n.playersRange,
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (!widget.bloquearModo) ...[
|
|
// Modo de juego
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(18, 18, 18, 28),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10n.gameMode,
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 12),
|
|
SegmentedButton<bool>(
|
|
segments: [
|
|
ButtonSegment(
|
|
value: false,
|
|
label: Text(l10n.singleDevice),
|
|
icon: IconoFarolero(Icons.phone_android),
|
|
),
|
|
ButtonSegment(
|
|
value: true,
|
|
label: Text(l10n.multiDevice),
|
|
icon: IconoFarolero(Icons.devices),
|
|
),
|
|
],
|
|
selected: {_modoMultimovil},
|
|
onSelectionChanged: (valor) {
|
|
setState(() {
|
|
_modoMultimovil = valor.first;
|
|
if (_numImpostores > _maxImpostores) {
|
|
_numImpostores = _maxImpostores;
|
|
}
|
|
});
|
|
_agregarPerfilLocalSiNecesario();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
] else ...[
|
|
PanelFarolero(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
child: Row(
|
|
children: [
|
|
IconoFarolero(
|
|
_modoMultimovil ? Icons.devices : Icons.phone_android,
|
|
color: TemaApp.colorNaranja,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
_modoMultimovil
|
|
? l10n.multiDeviceGameLabel
|
|
: l10n.singleDeviceGameLabel,
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
// CategorÃÂa
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(18, 18, 18, 28),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10n.category,
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: DropdownButtonFormField<String>(
|
|
initialValue: _categoria,
|
|
decoration: InputDecoration(
|
|
prefixIcon: Padding(
|
|
padding: EdgeInsets.all(10),
|
|
child: Image.asset(
|
|
'assets/ui/generated/actions/action_category_cards.webp',
|
|
width: 24,
|
|
height: 24,
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
),
|
|
items: categorias.map((c) {
|
|
return DropdownMenuItem(
|
|
value: c,
|
|
child: Text(
|
|
BancoPalabras.nombreBonitoCategoria(c, l10n),
|
|
),
|
|
);
|
|
}).toList(),
|
|
onChanged: (v) => setState(() => _categoria = v!),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
if (!_modoMultimovil) ...[
|
|
// Jugadores
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(18, 18, 18, 28),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
l10n.playersCount(_jugadores.length),
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
Text(
|
|
l10n.playersRangeHint,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _controladorNombre,
|
|
decoration: InputDecoration(
|
|
hintText: l10n.playerNameHint,
|
|
prefixIcon: Padding(
|
|
padding: EdgeInsets.all(10),
|
|
child: Image.asset(
|
|
'assets/ui/generated/actions/action_add_player.webp',
|
|
width: 24,
|
|
height: 24,
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
),
|
|
textCapitalization: TextCapitalization.words,
|
|
onSubmitted: (_) => _agregarJugador(),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
IconButton.filled(
|
|
onPressed: _agregarJugador,
|
|
icon: IconoFarolero(Icons.add),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
..._jugadores.asMap().entries.map((e) {
|
|
final perfil = servicioPerfil.perfil;
|
|
final nombrePerfil = perfil.nombre.trim().isEmpty
|
|
? l10n.defaultPlayerName
|
|
: perfil.nombre.trim();
|
|
final inicialPerfil = nombrePerfil.isEmpty
|
|
? '?'
|
|
: nombrePerfil.substring(0, 1).toUpperCase();
|
|
final esPerfilLocal =
|
|
e.key == 0 && e.value == nombrePerfil;
|
|
return ListTile(
|
|
minLeadingWidth: 62,
|
|
leading: esPerfilLocal
|
|
? SizedBox(
|
|
width: 62,
|
|
height: 62,
|
|
child: AvatarFarolero(
|
|
texto: inicialPerfil,
|
|
assetPath: perfil.avatarAsset,
|
|
size: 52,
|
|
),
|
|
)
|
|
: CircleAvatar(
|
|
backgroundColor: TemaApp.colorTarjeta,
|
|
child: Text(
|
|
'${e.key + 1}',
|
|
style: const TextStyle(color: TemaApp.colorTexto),
|
|
),
|
|
),
|
|
title: Text(e.value),
|
|
subtitle: esPerfilLocal
|
|
? Text(l10n.mainDeviceUser)
|
|
: null,
|
|
trailing: esPerfilLocal
|
|
? IconoFarolero(Icons.lock, color: TemaApp.colorDorado)
|
|
: IconButton(
|
|
icon: IconoFarolero(
|
|
Icons.close,
|
|
color: TemaApp.colorAcento,
|
|
),
|
|
onPressed: () => _eliminarJugador(e.key),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
|
|
// Configuración de partida
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(18, 18, 18, 28),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10n.configuration,
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Número de impostores
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(l10n.impostors),
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
onPressed: _numImpostores > 1
|
|
? () => setState(() => _numImpostores--)
|
|
: null,
|
|
icon: IconoFarolero(Icons.remove_circle_outline),
|
|
),
|
|
Text(
|
|
'$_numImpostores',
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
IconButton(
|
|
onPressed: _numImpostores < _maxImpostores
|
|
? () => setState(() => _numImpostores++)
|
|
: null,
|
|
icon: IconoFarolero(Icons.add_circle_outline),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
|
|
// Pista para impostor
|
|
SwitchListTile(
|
|
title: Text(l10n.impostorClue),
|
|
subtitle: Text(l10n.impostorClueDescription),
|
|
value: _pistaImpostor,
|
|
onChanged: (v) => setState(() => _pistaImpostor = v),
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
|
|
// Los impostores se reconocen entre ellos
|
|
SwitchListTile(
|
|
title: Text(l10n.impostorsKnowEachOther),
|
|
subtitle: Text(
|
|
_numImpostores > 1
|
|
? l10n.impostorsKnowEachOtherDescription
|
|
: l10n.impostorsKnowEachOtherNeedsTwo,
|
|
),
|
|
value: _impostoresSeConocen,
|
|
onChanged: _numImpostores > 1
|
|
? (v) => setState(() => _impostoresSeConocen = v)
|
|
: null,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
|
|
// Temporizador
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(l10n.debateTime),
|
|
DropdownButton<int?>(
|
|
value: _tiempoDebate,
|
|
items: List.generate(
|
|
_opcionesTiempo.length,
|
|
(i) => DropdownMenuItem(
|
|
value: _opcionesTiempo[i],
|
|
child: Text(etiquetas[i]),
|
|
),
|
|
),
|
|
onChanged: (v) => setState(() => _tiempoDebate = v),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Botón iniciar
|
|
BotonFarolero(
|
|
texto: l10n.startGame,
|
|
icono: Icons.play_arrow,
|
|
assetIconPath: 'assets/ui/generated/actions/action_create_game.webp',
|
|
onPressed: (_modoMultimovil || _jugadores.length >= 3)
|
|
? _iniciarPartida
|
|
: null,
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CrearPartidaHeader extends StatelessWidget {
|
|
final String titulo;
|
|
final String subtitulo;
|
|
|
|
const _CrearPartidaHeader({
|
|
required this.titulo,
|
|
required this.subtitulo,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final ancho = constraints.maxWidth.clamp(320.0, 720.0).toDouble();
|
|
return Center(
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(maxWidth: ancho),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
AspectRatio(
|
|
aspectRatio: 2,
|
|
child: Image.asset(
|
|
'assets/ui/generated/create_game/create_game_header_art.webp',
|
|
fit: BoxFit.contain,
|
|
opacity: const AlwaysStoppedAnimation(0.96),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
titulo,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
|
color: TemaApp.colorDorado,
|
|
fontWeight: FontWeight.w900,
|
|
shadows: [
|
|
Shadow(
|
|
color: TemaApp.colorNaranja.withValues(alpha: 0.55),
|
|
blurRadius: 16,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
subtitulo,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: TemaApp.colorTextoSecundario,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|