Files
pluriwave/lib/pantallas/pantalla_inicio.dart
T
FreeTLab e1d1d6c639
Build & Deploy Pluriwave / Análisis de código (push) Successful in 21s
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 2m19s
feat(ui): refine navigation and sleep timer
2026-05-22 13:13:05 +02:00

333 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart' as shimmer;
import '../estado/estado_radio.dart';
import '../widgets/pluri_glass_surface.dart';
import '../widgets/pluri_icon.dart';
import '../widgets/pluri_layout.dart';
import '../widgets/pluri_premium_widgets.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
import 'reproducir_minimizado.dart';
/// Pantalla principal: emisoras populares y por género.
class PantallaInicio extends StatefulWidget {
const PantallaInicio({super.key});
@override
State<PantallaInicio> createState() => _PantallaInicioState();
}
class _PantallaInicioState extends State<PantallaInicio> {
static const _generos = [
'pop',
'rock',
'jazz',
'classical',
'electronic',
'news',
'talk',
'hip-hop',
'country',
'metal',
'reggae',
'latin',
];
String? _generoSeleccionado;
@override
Widget build(BuildContext context) {
final estado = context.watch<EstadoRadio>();
final theme = Theme.of(context);
return RefreshIndicator(
onRefresh: estado.cargarPopulares,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(child: _heroHeader(context, estado)),
SliverToBoxAdapter(child: _seccionCercanas(estado, theme)),
SliverToBoxAdapter(child: _seccionTendencias(estado, theme)),
SliverToBoxAdapter(child: _chipGeneros(context, theme)),
if (estado.error != null)
SliverToBoxAdapter(child: _errorBanner(estado, theme)),
SliverPadding(
padding: const EdgeInsets.fromLTRB(PluriLayout.horizontal, 0, PluriLayout.horizontal, PluriLayout.bottomChromeInset),
sliver: _gridEmisoras(estado),
),
],
),
);
}
Widget _heroHeader(BuildContext context, EstadoRadio estado) {
return PluriScreenHeader(
title: 'PluriWave',
subtitle: 'Radio global en vivo con senales limpias, favoritos inteligentes y una experiencia visual de concurso.',
glyph: PluriIconGlyph.home,
primaryActionLabel: 'Explorar emisoras',
onPrimaryAction: estado.cargarPopulares,
trailing: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
PluriStatusPill(
icon: Icons.public_rounded,
label: '${estado.emisorasInicio.length} radios',
accent: Theme.of(context).colorScheme.secondary,
),
const SizedBox(height: 8),
const PluriStatusPill(
icon: Icons.hd_rounded,
label: 'Calidad HD',
),
],
),
);
}
Widget _seccionCercanas(EstadoRadio estado, ThemeData theme) {
final pais = estado.paisCercanoDetectado;
return Padding(
padding: const EdgeInsets.fromLTRB(PluriLayout.horizontal, 8, PluriLayout.horizontal, 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(PluriLayout.horizontal, 8, PluriLayout.horizontal, 0),
child: PluriGlassSurface(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Radar en directo', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
SizedBox(
height: 56,
child:
estado.cargandoPopulares
? ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: 5,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (_, __) => _ChipShimmer(theme: theme),
)
: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: estado.tendencias.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, i) {
final e = estado.tendencias[i];
return ActionChip(
avatar: const Icon(
Icons.graphic_eq_rounded,
size: 18,
),
label: Text(e.nombre, maxLines: 1),
onPressed:
() => reproducirMinimizado(context, e),
).animate().fadeIn(delay: (i * 50).ms);
},
),
),
],
),
),
);
}
Widget _chipGeneros(BuildContext context, ThemeData theme) {
return Padding(
padding: const EdgeInsets.fromLTRB(PluriLayout.horizontal, 16, PluriLayout.horizontal, 8),
child: PluriGlassSurface(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Géneros', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 4,
children:
_generos.map((g) {
final seleccionado = _generoSeleccionado == g;
return FilterChip(
label: Text(g),
selected: seleccionado,
onSelected: (_) {
setState(() {
_generoSeleccionado = seleccionado ? null : g;
});
if (!seleccionado) {
context.read<EstadoRadio>().buscar(tag: g);
} else {
context.read<EstadoRadio>().cargarPopulares();
}
},
);
}).toList(),
),
],
),
),
);
}
Widget _errorBanner(EstadoRadio estado, ThemeData theme) {
return Padding(
padding: const EdgeInsets.all(16),
child: PluriGlassSurface(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Icon(Icons.wifi_off, color: theme.colorScheme.error),
const SizedBox(width: 8),
Expanded(child: Text(estado.error!)),
TextButton(
onPressed: estado.cargarPopulares,
child: const Text('Reintentar'),
),
],
),
),
);
}
Widget _gridEmisoras(EstadoRadio estado) {
final emisoras =
_generoSeleccionado != null
? estado.resultadosBusqueda
: estado.emisorasInicio;
final cargando =
estado.cargandoPopulares ||
(_generoSeleccionado != null && estado.cargandoBusqueda);
if (cargando) {
return SliverGrid(
delegate: SliverChildBuilderDelegate(
(_, __) => const TarjetaEmisoraShimmer(),
childCount: 12,
),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.78,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
);
}
if (emisoras.isEmpty) {
return const SliverFillRemaining(
child: PluriEmptyState(
glyph: PluriIconGlyph.home,
title: 'No hay emisoras disponibles',
subtitle: 'Proba refrescar o elegir otro género para volver a capturar señal.',
),
);
}
return SliverGrid(
delegate: SliverChildBuilderDelegate(
(context, i) => TarjetaEmisora(
emisora: emisoras[i],
onTap: () => reproducirMinimizado(context, emisoras[i]),
).animate().fadeIn(delay: (i * 30).ms).slideY(begin: 0.1),
childCount: emisoras.length,
),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.78,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
);
}
}
class _ChipShimmer extends StatelessWidget {
final ThemeData theme;
const _ChipShimmer({required this.theme});
@override
Widget build(BuildContext context) {
return shimmer.Shimmer.fromColors(
baseColor: theme.colorScheme.surfaceContainerHighest,
highlightColor: theme.colorScheme.surface,
child: Container(
width: 120,
height: 56,
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
),
),
);
}
}