feat(radio): add nearby discovery and paged search
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 1m34s
Build & Deploy Pluriwave / Análisis de código (push) Successful in 11s

This commit is contained in:
2026-05-20 23:22:15 +02:00
parent f888153aa9
commit 7fcd0f544e
13 changed files with 428 additions and 40 deletions
+92 -7
View File
@@ -8,7 +8,7 @@ import '../widgets/pluri_icon.dart';
import '../widgets/pluri_premium_widgets.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
import 'reproducir_y_abrir.dart';
import 'reproducir_minimizado.dart';
const _paises = [
('Espana', 'ES'),
@@ -106,6 +106,7 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
),
),
),
_seccionCercanas(estado),
_seccionFiltro(
'Pais',
_paises.map((p) => (p.$1, p.$2)).toList(),
@@ -129,6 +130,76 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
);
}
Widget _seccionCercanas(EstadoRadio estado) {
final theme = Theme.of(context);
final pais = estado.paisCercanoDetectado;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: PluriGlassSurface(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
pais == null ? 'Emisoras cercanas' : 'Emisoras cercanas ? $pais',
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w900,
),
),
),
TextButton.icon(
onPressed: estado.cargandoCercanas
? null
: estado.cargarEmisorasCercanas,
icon: estado.cargandoCercanas
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.my_location_rounded, size: 18),
label: const Text('Buscar cerca'),
),
],
),
if (estado.errorCercanas != null)
Text(
estado.errorCercanas!,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
),
if (estado.emisorasCercanas.isNotEmpty) ...[
const SizedBox(height: 8),
SizedBox(
height: 76,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: estado.emisorasCercanas.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, i) {
final emisora = estado.emisorasCercanas[i];
return SizedBox(
width: 260,
child: TarjetaEmisora(
emisora: emisora,
esCompacta: true,
onTap: () => reproducirMinimizado(context, emisora),
),
);
},
),
),
],
],
),
),
);
}
Widget _seccionFiltro(
String titulo,
List<(String, String)> opciones,
@@ -197,13 +268,27 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 120),
itemCount: resultados.length,
itemCount: resultados.length + (estado.hayMasBusqueda ? 1 : 0),
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, i) => TarjetaEmisora(
emisora: resultados[i],
esCompacta: true,
onTap: () => reproducirYAbrir(context, resultados[i]),
).animate().fadeIn(delay: (i * 20).ms).slideY(begin: 0.08),
itemBuilder: (context, i) {
if (i >= resultados.length) {
if (!estado.cargandoMasBusqueda) {
Future<void>.microtask(estado.cargarMasBusqueda);
}
return const Padding(
padding: EdgeInsets.all(18),
child: Center(child: CircularProgressIndicator()),
);
}
if (i >= resultados.length - 5 && estado.hayMasBusqueda) {
Future<void>.microtask(estado.cargarMasBusqueda);
}
return TarjetaEmisora(
emisora: resultados[i],
esCompacta: true,
onTap: () => reproducirMinimizado(context, resultados[i]),
).animate().fadeIn(delay: (i.clamp(0, 12) * 20).ms).slideY(begin: 0.08);
},
);
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ import '../widgets/pluri_icon.dart';
import '../widgets/pluri_premium_widgets.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
import 'reproducir_y_abrir.dart';
import 'reproducir_minimizado.dart';
class PantallaFavoritos extends StatelessWidget {
const PantallaFavoritos({super.key});
@@ -87,7 +87,7 @@ class PantallaFavoritos extends StatelessWidget {
key: Key(emisora.uuid),
emisora: emisora,
esCompacta: true,
onTap: () => reproducirYAbrir(context, emisora),
onTap: () => reproducirMinimizado(context, emisora),
),
),
IconButton.filledTonal(
+73 -3
View File
@@ -10,7 +10,7 @@ import '../widgets/pluri_icon.dart';
import '../widgets/pluri_premium_widgets.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
import 'reproducir_y_abrir.dart';
import 'reproducir_minimizado.dart';
/// Pantalla principal: emisoras populares y por género.
class PantallaInicio extends StatefulWidget {
@@ -49,6 +49,7 @@ class _PantallaInicioState extends State<PantallaInicio> {
slivers: [
SliverToBoxAdapter(child: _heroHeader(context, estado)),
const SliverToBoxAdapter(child: _AuroraWaveBanner()),
SliverToBoxAdapter(child: _seccionCercanas(estado, theme)),
SliverToBoxAdapter(child: _seccionTendencias(estado, theme)),
SliverToBoxAdapter(child: _chipGeneros(context, theme)),
if (estado.error != null)
@@ -87,6 +88,75 @@ class _PantallaInicioState extends State<PantallaInicio> {
);
}
Widget _seccionCercanas(EstadoRadio estado, ThemeData theme) {
final pais = estado.paisCercanoDetectado;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: PluriGlassSurface(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
pais == null ? 'Cerca de vos' : 'Cerca de vos ? $pais',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w900,
),
),
),
TextButton.icon(
onPressed: estado.cargandoCercanas
? null
: estado.cargarEmisorasCercanas,
icon: estado.cargandoCercanas
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.my_location_rounded, size: 18),
label: const Text('Detectar'),
),
],
),
if (estado.errorCercanas != null)
Text(
estado.errorCercanas!,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
),
if (estado.emisorasCercanas.isNotEmpty) ...[
const SizedBox(height: 8),
SizedBox(
height: 76,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: estado.emisorasCercanas.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, i) {
final emisora = estado.emisorasCercanas[i];
return SizedBox(
width: 260,
child: TarjetaEmisora(
emisora: emisora,
esCompacta: true,
onTap: () => reproducirMinimizado(context, emisora),
),
);
},
),
),
],
],
),
),
);
}
Widget _seccionTendencias(EstadoRadio estado, ThemeData theme) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
@@ -120,7 +190,7 @@ class _PantallaInicioState extends State<PantallaInicio> {
),
label: Text(e.nombre, maxLines: 1),
onPressed:
() => reproducirYAbrir(context, e),
() => reproducirMinimizado(context, e),
).animate().fadeIn(delay: (i * 50).ms);
},
),
@@ -227,7 +297,7 @@ class _PantallaInicioState extends State<PantallaInicio> {
delegate: SliverChildBuilderDelegate(
(context, i) => TarjetaEmisora(
emisora: emisoras[i],
onTap: () => reproducirYAbrir(context, emisoras[i]),
onTap: () => reproducirMinimizado(context, emisoras[i]),
).animate().fadeIn(delay: (i * 30).ms).slideY(begin: 0.1),
childCount: emisoras.length,
),
@@ -5,11 +5,8 @@ import 'package:provider/provider.dart';
import '../estado/estado_radio.dart';
import '../modelos/emisora.dart';
import 'pantalla_reproductor.dart';
Future<void> reproducirYAbrir(BuildContext context, Emisora emisora) async {
void reproducirMinimizado(BuildContext context, Emisora emisora) {
final estado = context.read<EstadoRadio>();
unawaited(estado.reproducir(emisora));
if (!context.mounted) return;
await PantallaReproductor.abrir(context, emisora);
}