fix(inicio): render Tus emisoras as a 2-column grid of 8

Audit 1.10 (t4 lines 82-88): the prototype shows a 2x2+ grid of 8
favorite stations; the build was a 260px-wide horizontal strip capped
at 6.

Raises the cap to 8 and replaces the horizontal ListView with a
GridView.builder (2 columns, gap 10). Adds a dedicated _CeldaTusEmisoras
cell (44px square art radius 11, name + genre) rather than reusing
TarjetaEmisora(esCompacta: true), since that widget always renders a
favorite button and a live badge that this prototype cell never draws.
This commit is contained in:
2026-07-30 00:10:11 +02:00
parent fcba592352
commit a49b2b6519
2 changed files with 184 additions and 19 deletions
+134 -19
View File
@@ -17,7 +17,6 @@ import '../widgets/pluri_premium_widgets.dart';
import '../widgets/pluri_root_header.dart';
import '../widgets/pluri_sleep_timer_sheet.dart';
import '../widgets/visualizador_audio.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
import 'pantalla_reproductor.dart';
import 'reproducir_minimizado.dart';
@@ -78,7 +77,10 @@ class _PantallaInicioState extends State<PantallaInicio> {
/// todas" switching the root tab via `EstadoNavegacionRaiz.irA` rather
/// than pushing a route (`app-navigation-shell` — Root-to-Root Switching
/// Without Push).
static const _capTusEmisoras = 6;
///
/// Audit 1.10 (t4 lines 82-88): the prototype shows a 2-column grid of 8,
/// not a 6-capped horizontal strip.
static const _capTusEmisoras = 8;
Widget _seccionTusEmisoras(
BuildContext context,
@@ -130,24 +132,26 @@ class _PantallaInicioState extends State<PantallaInicio> {
),
)
else
SizedBox(
height: 76,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: mostrados.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, i) {
final emisora = mostrados[i];
return SizedBox(
width: 260,
child: TarjetaEmisora(
emisora: emisora,
esCompacta: true,
onTap: () => reproducirMinimizado(context, emisora),
),
);
},
// Audit 1.10 (t4 lines 82-88): a 2-column grid, gap 10 — was a
// horizontal strip of 260px-wide compact rows.
GridView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: mostrados.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 2.6,
),
itemBuilder: (context, i) {
final emisora = mostrados[i];
return _CeldaTusEmisoras(
emisora: emisora,
onTap: () => reproducirMinimizado(context, emisora),
);
},
),
],
),
@@ -569,3 +573,114 @@ String? _metaEscuchar(Emisora emisora) {
];
return partes.isEmpty ? null : partes.join(' · ');
}
/// Audit 1.10 (t4 lines 82-88): the "Tus emisoras" grid cell — a 44px
/// square thumbnail (radius 11), name (13/w700/lh1.2) and genre
/// (11/55%). Deliberately NOT `TarjetaEmisora(esCompacta: true)`: that
/// widget always renders a favorite button and a live badge, neither of
/// which the prototype's grid cell draws (t4 line 84-87 is art + two text
/// lines, nothing else).
class _CeldaTusEmisoras extends StatelessWidget {
const _CeldaTusEmisoras({required this.emisora, required this.onTap});
final Emisora emisora;
final VoidCallback onTap;
static const _ladoArte = 44.0;
static const _radioArte = 11.0;
static const _radioCelda = 16.0;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context);
final stationName = localizedStationName(l10n, emisora.nombre);
final genero = emisora.generos.isNotEmpty ? emisora.generos.first : null;
return Semantics(
button: true,
label: l10n.stationSemanticLabel(stationName),
child: PluriGlassSurface(
borderRadius: BorderRadius.circular(_radioCelda),
padding: const EdgeInsets.all(8),
child: Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: BorderRadius.circular(_radioCelda),
onTap: onTap,
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(_radioArte),
child: SizedBox(
width: _ladoArte,
height: _ladoArte,
child: _arte(theme),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stationName,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
height: 1.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (genero != null)
Text(
genero,
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.onSurface.withValues(
alpha: 0.55,
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
),
),
);
}
Widget _arte(ThemeData theme) {
if (emisora.favicon != null && emisora.favicon!.isNotEmpty) {
return CachedNetworkImage(
imageUrl: emisora.favicon!,
fit: BoxFit.cover,
placeholder: (_, __) => _shimmer(theme),
errorWidget: (_, __, ___) => _iconoFallback(theme),
);
}
return _iconoFallback(theme);
}
Widget _shimmer(ThemeData theme) => shimmer.Shimmer.fromColors(
baseColor: theme.colorScheme.surfaceContainerHighest,
highlightColor: theme.colorScheme.surface,
child: Container(color: theme.colorScheme.surfaceContainerHighest),
);
Widget _iconoFallback(ThemeData theme) => Container(
color: theme.colorScheme.primaryContainer,
child: Icon(
Icons.radio_rounded,
size: 20,
color: theme.colorScheme.onPrimaryContainer,
),
);
}
+50
View File
@@ -125,6 +125,56 @@ void main() {
);
});
testWidgets(
'visual fidelity (audit 1.10): Tus emisoras is a 2-column grid showing '
'up to 8 stations, not a 6-capped horizontal strip',
(tester) async {
_setLargeSurfaceSize(tester);
final favoritos = FakeServicioFavoritos();
final estado = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: favoritos,
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadio(),
resolverArchivoCustom: _archivoCustomVacio,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
for (var i = 0; i < 8; i++) {
await favoritos.agregar(
emisoraDemo(uuid: 'grid-$i', nombre: 'Grid Estacion $i'),
);
}
await estado.cargarFavoritos();
await tester.pumpWidget(
_conProviders(estado, _testApp(const PantallaInicio())),
);
await _pumpStableFrame(tester);
for (var i = 0; i < 8; i++) {
expect(
find.text('Grid Estacion $i'),
findsOneWidget,
reason:
'prototype t4 lines 82-88: a grid of 8, the old strip capped '
'at 6',
);
}
final grid = tester.widget<GridView>(find.byType(GridView));
final delegate =
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
expect(
delegate.crossAxisCount,
2,
reason: 'prototype t4 line 82: grid-template-columns:1fr 1fr',
);
},
);
testWidgets('visual fidelity (audit 1.3): the hero art is 132 square', (
tester,
) async {