diff --git a/lib/pantallas/pantalla_inicio.dart b/lib/pantallas/pantalla_inicio.dart index 74888c3..1f58f79 100644 --- a/lib/pantallas/pantalla_inicio.dart +++ b/lib/pantallas/pantalla_inicio.dart @@ -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 { /// 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 { ), ) 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, + ), + ); +} diff --git a/test/pantallas/pantalla_inicio_test.dart b/test/pantallas/pantalla_inicio_test.dart index 8324dc6..8e9f8f7 100644 --- a/test/pantallas/pantalla_inicio_test.dart +++ b/test/pantallas/pantalla_inicio_test.dart @@ -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(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 {