Files
farolero/lib/tema/dialogo_confirmacion.dart
FreeTLab 56f07364c2
Build & Deploy Farolero / Análisis de código (push) Successful in 13s
Build & Deploy Farolero / Build APK + AAB release (push) Successful in 1m22s
feat: confirm before leaving a game, and fix untranslated strings
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.
2026-07-25 23:59:57 +02:00

171 lines
5.5 KiB
Dart

import 'package:flutter/material.dart';
import '../l10n/generated/app_localizations.dart';
import 'componentes_farolero.dart';
import 'tema_app.dart';
/// Quién está saliendo, que determina qué se pierde y por tanto qué se avisa.
enum ContextoSalida {
/// Un solo móvil: la partida vive en este dispositivo y se pierde entera.
unDispositivo,
/// El host de una partida multidispositivo: al salir la corta para todos.
host,
/// Un cliente: solo se va él y puede volver mientras la sala siga abierta.
cliente,
}
/// Diálogo de confirmación con el estilo de la app.
///
/// Devuelve `true` solo si el jugador confirma. Cerrarlo tocando fuera o con
/// el gesto de atrás cuenta como cancelar, que es lo que interesa cuando lo
/// que se está evitando es precisamente una salida accidental.
Future<bool> confirmarFarolero({
required BuildContext context,
required String titulo,
required String mensaje,
required String textoConfirmar,
required String textoCancelar,
IconData icono = Icons.warning_amber_rounded,
IconData iconoConfirmar = Icons.logout,
IconData iconoCancelar = Icons.play_arrow,
}) async {
final confirmado = await showDialog<bool>(
context: context,
barrierDismissible: true,
builder: (dialogContext) => Dialog(
backgroundColor: TemaApp.colorSuperficie,
insetPadding: const EdgeInsets.symmetric(horizontal: 28, vertical: 24),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
side: BorderSide(
color: TemaApp.colorNaranja.withValues(alpha: 0.45),
),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: TemaApp.colorAcento.withValues(alpha: 0.16),
border: Border.all(
color: TemaApp.colorAcento.withValues(alpha: 0.65),
width: 2,
),
),
child: IconoFarolero(
icono,
color: TemaApp.colorNaranja,
size: 38,
),
),
const SizedBox(height: 18),
Text(
titulo,
textAlign: TextAlign.center,
style: Theme.of(dialogContext).textTheme.titleLarge?.copyWith(
color: TemaApp.colorDorado,
),
),
const SizedBox(height: 10),
Text(
mensaje,
textAlign: TextAlign.center,
style: Theme.of(dialogContext).textTheme.bodyMedium?.copyWith(
color: TemaApp.colorTextoSecundario,
height: 1.35,
),
),
const SizedBox(height: 26),
// La acción segura va primero y destacada: si alguien llega aquí
// sin querer, lo cómodo tiene que ser quedarse.
BotonFarolero(
texto: textoCancelar,
icono: iconoCancelar,
onPressed: () => Navigator.of(dialogContext).pop(false),
),
const SizedBox(height: 12),
BotonFarolero.oscuro(
texto: textoConfirmar,
icono: iconoConfirmar,
onPressed: () => Navigator.of(dialogContext).pop(true),
),
],
),
),
),
);
return confirmado ?? false;
}
/// Pregunta antes de abandonar una partida en curso.
Future<bool> confirmarSalirDePartida(
BuildContext context,
ContextoSalida contexto,
) {
final l10n = AppLocalizations.of(context)!;
return confirmarFarolero(
context: context,
titulo: l10n.leaveGameTitle,
mensaje: switch (contexto) {
ContextoSalida.unDispositivo => l10n.leaveGameSingleDevice,
ContextoSalida.host => l10n.leaveGameHost,
ContextoSalida.cliente => l10n.leaveGameClient,
},
textoConfirmar: l10n.leaveGame,
textoCancelar: l10n.stayInGame,
);
}
/// Envuelve una pantalla de partida para que el gesto o botón de atrás no la
/// abandone sin preguntar.
///
/// Se encarga solo del atrás del sistema; los botones de cerrar de cada
/// pantalla llaman a [confirmarSalirDePartida] por su cuenta, porque además de
/// confirmar tienen que desconectar y navegar.
class GuardiaSalidaPartida extends StatelessWidget {
final Widget child;
final ContextoSalida contexto;
/// Qué hacer cuando el jugador confirma que quiere salir. Si es null se deja
/// que la ruta se cierre con normalidad.
final VoidCallback? onSalirConfirmado;
/// Permite desactivar la guarda sin sacar el widget del árbol, por ejemplo
/// cuando la partida ya ha terminado.
final bool activo;
const GuardiaSalidaPartida({
super.key,
required this.child,
required this.contexto,
this.onSalirConfirmado,
this.activo = true,
});
@override
Widget build(BuildContext context) {
return PopScope(
canPop: !activo,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final salir = await confirmarSalirDePartida(context, contexto);
if (!salir || !context.mounted) return;
final alSalir = onSalirConfirmado;
if (alSalir != null) {
alSalir();
} else if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
child: child,
);
}
}