fix(buscar): build the 2x2 "Explorar por" grid, add Novedades
Audit 3.2 (t4 lines 162-171): the prototype groups Países, Géneros, Tendencias and the entirely-missing Novedades into one 2x2 entry-point grid. The build had them as three unrelated always-visible widgets (a Países ListTile, a Géneros FilterChip Wrap, a Tendencias ActionChip strip) and no Novedades entry at all. Replaces all three with a single grid section, capability-preserving: Países still pushes PantallaPaises; Géneros and Tendencias now open their exact existing content in a picker sheet instead of always-on- screen (Géneros auto-closes on selection, matching this screen's other single-choice filter sheets; Tendencias stays open, a browse list, not a filter). Novedades re-triggers the existing discovery refresh, since no distinct "new stations" feed exists anywhere in the domain. New ARB key set (exploreByTitle/exploreTrendingTitle/ exploreTrendingSubtitle/exploreNewTitle/exploreNewSubtitle) translated across all 13 locales with zero anti-copy allowlist entries needed. Also fixes a pre-existing PluriEmptyState overflow this restructure exposed (unrelated to the grid itself, confirmed via isolated repro): wraps its Column in a SingleChildScrollView so a too-tall title/ subtitle scrolls instead of throwing a hard RenderFlex overflow.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart' as shimmer;
|
||||
@@ -8,6 +10,7 @@ import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../tema/pluri_animate.dart';
|
||||
import '../tema/pluriwave_theme.dart';
|
||||
import '../tema/pluriwave_tokens.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/pluri_layout.dart';
|
||||
@@ -199,9 +202,11 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
_resultados(estado, theme),
|
||||
] else ...[
|
||||
_seccionCercanas(context, theme, l10n),
|
||||
_seccionTendencias(context, theme, l10n),
|
||||
_chipGeneros(context, theme, l10n),
|
||||
_seccionPaises(context, theme, l10n),
|
||||
// Audit 3.2 (t4 lines 162-171): a single "Explorar por" 2x2 grid
|
||||
// replaces the always-visible Tendencias chip strip, Géneros
|
||||
// chip Wrap and Países ListTile — each cell keeps its EXACT
|
||||
// existing capability, just behind a tap instead of always-on.
|
||||
_seccionExplorarPor(context, theme, l10n),
|
||||
if (context.select<EstadoRadio, String?>((e) => e.error) != null)
|
||||
_errorBanner(
|
||||
context,
|
||||
@@ -798,6 +803,12 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
} else {
|
||||
context.read<EstadoRadio>().cargarPopulares();
|
||||
}
|
||||
// Audit 3.2: only reachable from the "Explorar por"
|
||||
// Géneros sheet now — tap-once-and-close, matching
|
||||
// this screen's other single-choice filter sheets.
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
@@ -808,43 +819,157 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
);
|
||||
}
|
||||
|
||||
/// WU7, `station-discovery-browse` spec: the "Países entry point" the
|
||||
/// spec's landing-state scenario lists. Deferred from WU6 (this class's
|
||||
/// own doc comment: `PantallaPaises` did not exist yet); added here now
|
||||
/// that it does, alongside the screen it targets — an unreachable screen
|
||||
/// would repeat the WU15/WU15b lesson (a fully-tested screen shipped with
|
||||
/// no navigation path to it).
|
||||
Widget _seccionPaises(
|
||||
/// Audit 3.2 (t4 lines 162-171): "Explorar por" 2x2 grid. Replaces the
|
||||
/// old always-visible Países `ListTile` (WU7), Géneros chip `Wrap` and
|
||||
/// Tendencias chip strip — 4 cells, adding the entirely-missing
|
||||
/// Novedades entry. Task constraint: presentation changes, capability
|
||||
/// does not — Países still pushes `PantallaPaises` (WU7's own reasoning
|
||||
/// for why that screen must stay reachable still applies); Géneros and
|
||||
/// Tendencias now open their EXACT existing content in a picker sheet
|
||||
/// instead of always-on-screen, so no selection logic is duplicated.
|
||||
Widget _seccionExplorarPor(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
8,
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
),
|
||||
child: PluriGlassSurface(
|
||||
padding: EdgeInsets.zero,
|
||||
// `PluriGlassSurface` paints via `DecoratedBox`, not `Material` — a
|
||||
// tappable `ListTile` needs its own `Material` ancestor or its ink
|
||||
// splash silently fails to paint (Flutter's own debug assertion).
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.public_rounded),
|
||||
title: Text(l10n.countriesScreenTitle),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap:
|
||||
() => PluriPushScaffold.push(
|
||||
context,
|
||||
(_) => const PantallaPaises(),
|
||||
),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.titleHorizontal,
|
||||
8,
|
||||
PluriLayout.titleHorizontal,
|
||||
0,
|
||||
),
|
||||
child: Text(
|
||||
l10n.exploreByTitle,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
key: const Key('explore-by-grid'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: PluriLayout.horizontal,
|
||||
),
|
||||
// A manually-built 2-per-row layout, NOT `GridView` +
|
||||
// `childAspectRatio`: a fixed aspect ratio sizes each cell's
|
||||
// height as a function of the SCREEN width, which overflowed at
|
||||
// a narrow/default test viewport even though the same ratio fit
|
||||
// fine at the wider viewport this was first verified against.
|
||||
// `Expanded` cells size their height from their OWN content,
|
||||
// which cannot overflow this way at any width.
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _CeldaExplorarPor(
|
||||
key: const Key('explore-cell-paises'),
|
||||
icon: Icons.public_rounded,
|
||||
color: PluriWaveTokens.brand,
|
||||
title: l10n.countriesScreenTitle,
|
||||
// No subtitle: a live count needs
|
||||
// EstadoBusqueda.cargarPaises() triggered from this
|
||||
// screen, which was tried and reverted — it forced
|
||||
// an extra rebuild that (harmlessly in production,
|
||||
// but fatally under the default 800-wide test
|
||||
// viewport) surfaced a pre-existing, unrelated
|
||||
// PluriEmptyState overflow in the empty discovery
|
||||
// grid below. Not worth the coupling for a cosmetic
|
||||
// badge.
|
||||
subtitle: null,
|
||||
onTap:
|
||||
() => PluriPushScaffold.push(
|
||||
context,
|
||||
(_) => const PantallaPaises(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _CeldaExplorarPor(
|
||||
key: const Key('explore-cell-generos'),
|
||||
icon: Icons.library_music_rounded,
|
||||
color: context.pluriTokens.liveGreen,
|
||||
title: l10n.genresTitle,
|
||||
subtitle: '${_generos.length}',
|
||||
onTap: () => _abrirGenerosSheet(theme, l10n),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _CeldaExplorarPor(
|
||||
key: const Key('explore-cell-tendencias'),
|
||||
icon: Icons.trending_up_rounded,
|
||||
color: context.pluriTokens.warmCoral,
|
||||
title: l10n.exploreTrendingTitle,
|
||||
subtitle: l10n.exploreTrendingSubtitle,
|
||||
onTap: () => _abrirTendenciasSheet(theme, l10n),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _CeldaExplorarPor(
|
||||
key: const Key('explore-cell-novedades'),
|
||||
icon: Icons.fiber_new_rounded,
|
||||
color: PluriWaveTokens.skyBlue,
|
||||
title: l10n.exploreNewTitle,
|
||||
subtitle: l10n.exploreNewSubtitle,
|
||||
// No distinct "new stations" feed exists in the
|
||||
// domain (ServicioRadio/EstadoRadio have no such
|
||||
// concept, and Emisora carries no added/changed
|
||||
// timestamp) — this re-runs the SAME discovery
|
||||
// refresh the offline banner's retry action already
|
||||
// calls, rather than inventing one.
|
||||
onTap:
|
||||
() => unawaited(
|
||||
context.read<EstadoRadio>().cargarPopulares(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Opens [_chipGeneros]'s EXACT existing content/selection logic behind
|
||||
/// a tap instead of always-on-screen — same capability, same state
|
||||
/// (`_generoSeleccionado`), same calls (`EstadoBusqueda.buscar(tag:)` /
|
||||
/// `EstadoRadio.cargarPopulares()`). Auto-closes on selection, matching
|
||||
/// this screen's country/language/quality filter sheets (single-choice
|
||||
/// picker, not a browse list).
|
||||
Future<void> _abrirGenerosSheet(ThemeData theme, AppLocalizations l10n) {
|
||||
return showModalBottomSheet(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (ctx) => SafeArea(child: _chipGeneros(ctx, theme, l10n)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Opens [_seccionTendencias]'s EXACT existing content/tap-to-play logic
|
||||
/// behind a tap. Deliberately does NOT auto-close on selection — unlike
|
||||
/// Géneros, this is a browse-and-preview list (tapping a station starts
|
||||
/// playback in place, the same as tapping any station card elsewhere in
|
||||
/// the app; it does not "choose" a single filter value).
|
||||
Future<void> _abrirTendenciasSheet(ThemeData theme, AppLocalizations l10n) {
|
||||
return showModalBottomSheet(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (ctx) => SafeArea(child: _seccionTendencias(ctx, theme, l10n)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1037,3 +1162,76 @@ class _ChipShimmer extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Audit 3.2 (t4 lines 167-170): one "Explorar por" grid cell — icon,
|
||||
/// title (13.5/w800) and subtitle (11/55%). Radius 16 is a local one-off
|
||||
/// (like `_errorBanner`'s), matching neither of the 3 named token radii.
|
||||
class _CeldaExplorarPor extends StatelessWidget {
|
||||
const _CeldaExplorarPor({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final VoidCallback onTap;
|
||||
|
||||
static const _radio = 16.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return PluriGlassSurface(
|
||||
borderRadius: BorderRadius.circular(_radio),
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(_radio),
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 24, color: color),
|
||||
const SizedBox(width: 11),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: theme.colorScheme.onSurface.withValues(
|
||||
alpha: 0.55,
|
||||
),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user