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.
250 lines
8.3 KiB
Dart
250 lines
8.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
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';
|
|
|
|
/// WU7, `station-discovery-browse` spec — "Países Browser Over the Verified
|
|
/// Countries Contract". Pure display over `EstadoBusqueda.cargarPaises()`
|
|
/// (in-memory cache guard, so re-entering this screen never re-fetches):
|
|
/// a "Tus idiomas" shortlist above the full alphabetical list, both with
|
|
/// 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, 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();
|
|
}
|
|
|
|
class _PantallaPaisesState extends State<PantallaPaises> {
|
|
/// One representative country per app-supported locale (the same 13
|
|
/// locales as `pantalla_ajustes_idioma.dart`'s `_idiomas` list). "Tus
|
|
/// idiomas" is named by the proposal/spec but its derivation is not
|
|
/// otherwise specified — this reuses the app's own existing language
|
|
/// identity rather than inventing a separate curated country list.
|
|
static const _paisPorIdioma = <String, String>{
|
|
'en': 'US',
|
|
'es': 'ES',
|
|
'zh': 'CN',
|
|
'hi': 'IN',
|
|
'ar': 'SA',
|
|
'pt': 'PT',
|
|
'fr': 'FR',
|
|
'ru': 'RU',
|
|
'de': 'DE',
|
|
'ja': 'JP',
|
|
'id': 'ID',
|
|
'bn': 'BD',
|
|
'it': 'IT',
|
|
};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// `cargarPaises()` calls `notifyListeners()` before its first `await`
|
|
// (its loading-flag flip) — doing that synchronously inside `initState`
|
|
// would trigger "setState() or markNeedsBuild() called during build."
|
|
// Deferred to a post-frame callback, matching the established pattern
|
|
// for triggering a load from a screen's `initState`/`build`
|
|
// (`pantalla_alarmas.dart`'s `_favoritosSolicitados` guard).
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) context.read<EstadoBusqueda>().cargarPaises();
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context);
|
|
final estado = context.watch<EstadoBusqueda>();
|
|
|
|
return PluriPushScaffold(
|
|
title: l10n.countriesScreenTitle,
|
|
body:
|
|
estado.cargandoPaises && estado.paises.isEmpty
|
|
? const Center(child: CircularProgressIndicator())
|
|
: ListView(
|
|
padding: PluriLayout.pageContentPadding,
|
|
children: [
|
|
_seccionTusIdiomas(context, estado.paises, l10n),
|
|
const SizedBox(height: 16),
|
|
_seccionTodos(context, estado.paises, l10n),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _seleccionar(PaisRadio pais) {
|
|
widget.onPaisSeleccionado?.call(pais);
|
|
}
|
|
|
|
Widget _seccionTusIdiomas(
|
|
BuildContext context,
|
|
List<PaisRadio> paises,
|
|
AppLocalizations l10n,
|
|
) {
|
|
final porCodigo = {for (final p in paises) p.codigoIso: p};
|
|
final destacados =
|
|
_paisPorIdioma.values
|
|
.map((codigo) => porCodigo[codigo])
|
|
.whereType<PaisRadio>()
|
|
.toList();
|
|
|
|
if (destacados.isEmpty) return const SizedBox.shrink();
|
|
|
|
final theme = Theme.of(context);
|
|
return PluriGlassSurface(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10n.countriesYourLanguagesTitle,
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
// 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),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _seccionTodos(
|
|
BuildContext context,
|
|
List<PaisRadio> paises,
|
|
AppLocalizations l10n,
|
|
) {
|
|
final theme = Theme.of(context);
|
|
return PluriGlassSurface(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10n.countriesAllTitle,
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
// 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),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|