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_busqueda.dart'; import '../estado/estado_radio.dart'; import '../l10n/gen/app_localizations.dart'; import '../modelos/emisora.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 createState() => _PantallaInicioState(); } class _PantallaInicioState extends State { static const _generos = [ 'pop', 'rock', 'jazz', 'classical', 'electronic', 'news', 'talk', 'hip-hop', 'country', 'metal', 'reggae', 'latin', ]; String? _generoSeleccionado; @override Widget build(BuildContext context) { // S4-R5: no root watch on EstadoRadio. Every field is consumed through // context.select over identity-memoized getters, so audio buffer events // (which notify EstadoRadio) no longer rebuild this screen. final theme = Theme.of(context); final l10n = AppLocalizations.of(context); final error = context.select((e) => e.error); return RefreshIndicator( onRefresh: () => context.read().cargarPopulares(), child: CustomScrollView( slivers: [ SliverToBoxAdapter(child: _heroHeader(context, l10n)), SliverToBoxAdapter(child: _seccionCercanas(context, theme, l10n)), SliverToBoxAdapter(child: _seccionTendencias(context, theme, l10n)), SliverToBoxAdapter(child: _chipGeneros(context, theme, l10n)), if (error != null) SliverToBoxAdapter( child: _errorBanner(context, error, theme, l10n), ), SliverPadding( padding: const EdgeInsets.fromLTRB( PluriLayout.horizontal, 0, PluriLayout.horizontal, PluriLayout.bottomChromeInset, ), sliver: _gridEmisoras(context, l10n), ), ], ), ); } Widget _heroHeader(BuildContext context, AppLocalizations l10n) { final totalEmisoras = context.select( (e) => e.emisorasInicio.length, ); return PluriScreenHeader( title: l10n.appTitle, subtitle: l10n.homeScreenSubtitle, glyph: PluriIconGlyph.home, primaryActionLabel: l10n.exploreStations, onPrimaryAction: () => context.read().cargarPopulares(), trailing: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ PluriStatusPill( icon: Icons.public_rounded, label: l10n.stationsCount(totalEmisoras), accent: Theme.of(context).colorScheme.secondary, ), const SizedBox(height: 8), PluriStatusPill(icon: Icons.hd_rounded, label: l10n.qualityHd), ], ), ); } Widget _seccionCercanas( BuildContext context, ThemeData theme, AppLocalizations l10n, ) { // Nearby stations live in EstadoBusqueda (S4-R3). final busqueda = context.watch(); final pais = busqueda.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 ? l10n.nearYou : l10n.nearYouInCountry(pais), style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, ), ), ), TextButton.icon( onPressed: busqueda.cargandoCercanas ? null : busqueda.cargarEmisorasCercanas, icon: busqueda.cargandoCercanas ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.my_location_rounded, size: 18), label: Text(l10n.detectAction), ), ], ), if (busqueda.errorCercanas != null) Text( busqueda.errorCercanas!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.error, ), ), if (busqueda.cercanas.isNotEmpty) ...[ const SizedBox(height: 8), SizedBox( height: 76, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: busqueda.cercanas.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (context, i) { final emisora = busqueda.cercanas[i]; return SizedBox( width: 260, child: TarjetaEmisora( emisora: emisora, esCompacta: true, onTap: () => reproducirMinimizado(context, emisora), ), ); }, ), ), ], ], ), ), ); } Widget _seccionTendencias( BuildContext context, ThemeData theme, AppLocalizations l10n, ) { final cargando = context.select( (e) => e.cargandoPopulares, ); final tendencias = context.select>( (e) => e.tendencias, ); 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(l10n.liveRadar, style: theme.textTheme.titleMedium), const SizedBox(height: 8), SizedBox( height: 56, child: cargando ? ListView.separated( scrollDirection: Axis.horizontal, itemCount: 5, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (_, __) => _ChipShimmer(theme: theme), ) : ListView.separated( scrollDirection: Axis.horizontal, itemCount: tendencias.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (context, i) { final e = 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, AppLocalizations l10n, ) { 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(l10n.genresTitle, 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(_genreName(l10n, g)), selected: seleccionado, onSelected: (_) { setState(() { _generoSeleccionado = seleccionado ? null : g; }); if (!seleccionado) { context.read().buscar(tag: g); } else { context.read().cargarPopulares(); } }, ); }).toList(), ), ], ), ), ); } Widget _errorBanner( BuildContext context, String error, ThemeData theme, AppLocalizations l10n, ) { 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(error)), TextButton( onPressed: () => context.read().cargarPopulares(), child: Text(l10n.retryAction), ), ], ), ), ); } Widget _gridEmisoras(BuildContext context, AppLocalizations l10n) { final porGenero = _generoSeleccionado != null; final emisoras = porGenero ? context.select>((b) => b.resultados) : context.select>( (e) => e.emisorasInicio, ); final cargando = context.select((e) => e.cargandoPopulares) || (porGenero && context.select((b) => b.cargando)); 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 SliverFillRemaining( child: PluriEmptyState( glyph: PluriIconGlyph.home, title: l10n.noStationsAvailable, subtitle: l10n.noStationsAvailableSubtitle, ), ); } 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, ), ); } } String _genreName(AppLocalizations l10n, String tag) => switch (tag) { 'pop' => l10n.genrePop, 'rock' => l10n.genreRock, 'jazz' => l10n.genreJazz, 'classical' => l10n.genreClassical, 'electronic' => l10n.genreElectronic, 'news' => l10n.genreNews, 'talk' => l10n.genreTalk, 'hip-hop' => l10n.genreHipHop, 'country' => l10n.genreCountry, 'metal' => l10n.genreMetal, 'reggae' => l10n.genreReggae, 'latin' => l10n.genreLatin, _ => tag, }; 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), ), ), ); } }