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:
@@ -17,7 +17,6 @@ import '../widgets/pluri_premium_widgets.dart';
|
|||||||
import '../widgets/pluri_root_header.dart';
|
import '../widgets/pluri_root_header.dart';
|
||||||
import '../widgets/pluri_sleep_timer_sheet.dart';
|
import '../widgets/pluri_sleep_timer_sheet.dart';
|
||||||
import '../widgets/visualizador_audio.dart';
|
import '../widgets/visualizador_audio.dart';
|
||||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
|
||||||
|
|
||||||
import 'pantalla_reproductor.dart';
|
import 'pantalla_reproductor.dart';
|
||||||
import 'reproducir_minimizado.dart';
|
import 'reproducir_minimizado.dart';
|
||||||
@@ -78,7 +77,10 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
|||||||
/// todas" switching the root tab via `EstadoNavegacionRaiz.irA` rather
|
/// todas" switching the root tab via `EstadoNavegacionRaiz.irA` rather
|
||||||
/// than pushing a route (`app-navigation-shell` — Root-to-Root Switching
|
/// than pushing a route (`app-navigation-shell` — Root-to-Root Switching
|
||||||
/// Without Push).
|
/// 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(
|
Widget _seccionTusEmisoras(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -130,24 +132,26 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
SizedBox(
|
// Audit 1.10 (t4 lines 82-88): a 2-column grid, gap 10 — was a
|
||||||
height: 76,
|
// horizontal strip of 260px-wide compact rows.
|
||||||
child: ListView.separated(
|
GridView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
padding: EdgeInsets.zero,
|
||||||
itemCount: mostrados.length,
|
shrinkWrap: true,
|
||||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, i) {
|
itemCount: mostrados.length,
|
||||||
final emisora = mostrados[i];
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
return SizedBox(
|
crossAxisCount: 2,
|
||||||
width: 260,
|
mainAxisSpacing: 10,
|
||||||
child: TarjetaEmisora(
|
crossAxisSpacing: 10,
|
||||||
emisora: emisora,
|
childAspectRatio: 2.6,
|
||||||
esCompacta: true,
|
|
||||||
onTap: () => reproducirMinimizado(context, emisora),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
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(' · ');
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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', (
|
testWidgets('visual fidelity (audit 1.3): the hero art is 132 square', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|||||||
Reference in New Issue
Block a user