fix(paises,buscar): tappable ISO rows and a reconnect card

Item 24 / audit 5.1-5.3, 5.5 (t4:255-269): Paises' "Tus idiomas" was
a Wrap of non-interactive Chips, and the full list was a plain
ListTile with no ISO column and no onTap. Both now share one tappable
row (ISO code, name, station count, chevron); the first "Tus idiomas"
row gets the prototype's teal-tinted highlight. The one production
call site wires the tap to filter Buscar by that country's code and
pop back -- EstadoBusqueda.buscar(pais: ...) already accepts any ISO
alpha-2 code, not just the ~10 presets in the filter sheet.

Item 25 / audit 13.2 (t4:643-646): a reconnect card (rotating ring,
station name, "Reconectando...", a stop affordance) replaces the
complete absence of any reconnect signal outside a word in the mini
player. Ships WITHOUT the prototype's attempt counter: the only live
ControladorReconexion instance is a private field of
PluriWaveAudioHandler inside servicio_audio.dart, a file this task
requires stay byte-identical to main, and nothing else re-exposes it.
Reconstructing a count from estadoStream's reconectando emissions
would not be faithful (the stream can emit it many times per actual
backoff attempt), so that was deliberately not attempted.
This commit is contained in:
2026-07-30 11:48:25 +02:00
parent 955682271c
commit 3303bd387c
4 changed files with 676 additions and 38 deletions
+115 -27
View File
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import '../estado/estado_busqueda.dart';
import '../l10n/gen/app_localizations.dart';
import '../modelos/pais_radio.dart';
import '../tema/pluriwave_tokens.dart';
import '../widgets/pluri_glass_surface.dart';
import '../widgets/pluri_layout.dart';
import '../widgets/pluri_push_scaffold.dart';
@@ -15,7 +16,14 @@ import '../widgets/pluri_push_scaffold.dart';
/// live station counts already parsed from the API's `stationcount`
/// **string** field (`PaisRadio.fromApi`, Engram id 2500).
class PantallaPaises extends StatefulWidget {
const PantallaPaises({super.key});
const PantallaPaises({super.key, this.onPaisSeleccionado});
/// Item 24 / audit 5.2 (t4:256-269): every row is tappable (the prototype
/// draws a `chevron_right` on all of them). Optional so this screen stays
/// usable stand-alone; the one production call site
/// (`pantalla_buscar.dart`'s "Explorar por" grid) wires this to filter
/// search results by the tapped country and pop back.
final ValueChanged<PaisRadio>? onPaisSeleccionado;
@override
State<PantallaPaises> createState() => _PantallaPaisesState();
@@ -78,6 +86,10 @@ class _PantallaPaisesState extends State<PantallaPaises> {
);
}
void _seleccionar(PaisRadio pais) {
widget.onPaisSeleccionado?.call(pais);
}
Widget _seccionTusIdiomas(
BuildContext context,
List<PaisRadio> paises,
@@ -105,18 +117,16 @@ class _PantallaPaisesState extends State<PantallaPaises> {
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final pais in destacados)
Chip(
label: Text(
'${pais.nombre} · ${l10n.stationsCount(pais.numeroEmisoras)}',
),
),
],
),
// Item 24 / audit 5.1 (t4:255-258): a column of tappable ISO
// rows, not a Wrap of non-interactive Chips.
for (final pais in destacados)
_FilaPais(
pais: pais,
l10n: l10n,
// Item 24 / audit 5.5 (t4:256): the first row is highlighted.
destacado: pais == destacados.first,
onTap: () => _seleccionar(pais),
),
],
),
);
@@ -140,22 +150,100 @@ class _PantallaPaisesState extends State<PantallaPaises> {
),
),
const SizedBox(height: 4),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: paises.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, i) {
final pais = paises[i];
return ListTile(
contentPadding: EdgeInsets.zero,
title: Text(pais.nombre),
trailing: Text(l10n.stationsCount(pais.numeroEmisoras)),
);
},
),
// Item 24 / audit 5.1-5.3 (t4:262-269): the same tappable ISO row
// as "Tus idiomas" -- not the previous ListTile, which had no ISO
// column and no onTap.
for (final pais in paises)
_FilaPais(pais: pais, l10n: l10n, onTap: () => _seleccionar(pais)),
],
),
);
}
}
/// Item 24 / audit 5.1-5.3, 5.5 (t4:256-269): a tappable row -- ISO code
/// column, name, station count, and a chevron -- shared by "Tus idiomas"
/// and "Todos". [destacado] applies the t4:256 highlight (teal-tinted
/// background, bold name) reserved for the very first "Tus idiomas" row.
class _FilaPais extends StatelessWidget {
const _FilaPais({
required this.pais,
required this.l10n,
this.destacado = false,
this.onTap,
});
final PaisRadio pais;
final AppLocalizations l10n;
final bool destacado;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final onSurface = Theme.of(context).colorScheme.onSurface;
return DecoratedBox(
decoration: BoxDecoration(
color:
destacado
? PluriWaveTokens.brand.withValues(alpha: 0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(14),
),
child: Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
// t4:256-258: 26px/lh1, centred in a 34-wide column.
SizedBox(
width: 34,
child: Text(
pais.codigoIso,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 26, height: 1),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
pais.nombre,
style: TextStyle(
fontSize: 15.5,
fontWeight:
destacado ? FontWeight.w800 : FontWeight.w700,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
l10n.stationsCount(pais.numeroEmisoras),
style: TextStyle(
fontSize: 12,
color: onSurface.withValues(alpha: 0.55),
),
),
],
),
),
Icon(
Icons.chevron_right_rounded,
size: 20,
color: onSurface.withValues(alpha: 0.4),
),
],
),
),
),
),
);
}
}