feat(ui): add premium PluriWave redesign
This commit is contained in:
+123
-117
@@ -1,8 +1,12 @@
|
||||
import 'package:flutter/material.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 '../tema/pluriwave_theme.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/tarjeta_emisora.dart';
|
||||
|
||||
/// Pantalla principal: emisoras populares y por género.
|
||||
@@ -24,93 +28,117 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
Widget build(BuildContext context) {
|
||||
final estado = context.watch<EstadoRadio>();
|
||||
final theme = Theme.of(context);
|
||||
final t = context.pluriTokens;
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: estado.cargarPopulares,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: _seccionTendencias(estado, theme),
|
||||
SliverToBoxAdapter(child: _heroHeader(context)),
|
||||
SliverToBoxAdapter(child: _seccionTendencias(estado, theme)),
|
||||
SliverToBoxAdapter(child: _chipGeneros(context, theme)),
|
||||
if (estado.error != null) SliverToBoxAdapter(child: _errorBanner(estado, theme)),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: t.spacingMd),
|
||||
sliver: _gridEmisoras(estado),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: _chipGeneros(theme),
|
||||
),
|
||||
if (estado.error != null)
|
||||
SliverToBoxAdapter(
|
||||
child: _errorBanner(estado, theme),
|
||||
),
|
||||
_gridEmisoras(estado, theme),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _heroHeader(BuildContext context) {
|
||||
final t = context.pluriTokens;
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(t.spacingMd, t.spacingSm, t.spacingMd, t.spacingSm),
|
||||
child: PluriGlassSurface(
|
||||
borderRadius: BorderRadius.circular(t.radiusLg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const PluriIcon(glyph: PluriIconGlyph.home, variant: PluriIconVariant.activeGlow, size: 30),
|
||||
const SizedBox(height: 10),
|
||||
Text('PluriWave', style: theme.textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700)),
|
||||
Text('Ondas vivas globales', style: theme.textTheme.titleMedium?.copyWith(color: t.warmCoral)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _seccionTendencias(EstadoRadio estado, ThemeData theme) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('🔥 Tendencias', 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.radio, size: 18),
|
||||
label: Text(e.nombre, maxLines: 1),
|
||||
onPressed: () => context.read<EstadoRadio>().reproducir(e),
|
||||
).animate().fadeIn(delay: (i * 50).ms);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Tendencias premium', 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: () => context.read<EstadoRadio>().reproducir(e),
|
||||
).animate().fadeIn(delay: (i * 50).ms);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _chipGeneros(ThemeData theme) {
|
||||
Widget _chipGeneros(BuildContext context, ThemeData theme) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
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(),
|
||||
),
|
||||
],
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -118,44 +146,27 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
Widget _errorBanner(EstadoRadio estado, ThemeData theme) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Card(
|
||||
color: theme.colorScheme.errorContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.wifi_off, color: theme.colorScheme.onErrorContainer),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
estado.error!,
|
||||
style: TextStyle(color: theme.colorScheme.onErrorContainer),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: estado.cargarPopulares,
|
||||
child: const Text('Reintentar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
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, ThemeData theme) {
|
||||
final emisoras = _generoSeleccionado != null
|
||||
? estado.resultadosBusqueda
|
||||
: estado.emisorasInicio;
|
||||
final cargando = estado.cargandoPopulares ||
|
||||
(_generoSeleccionado != null && estado.cargandoBusqueda);
|
||||
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,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((_, __) => const TarjetaEmisoraShimmer(), childCount: 12),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.85,
|
||||
@@ -166,27 +177,22 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
}
|
||||
|
||||
if (emisoras.isEmpty) {
|
||||
return const SliverFillRemaining(
|
||||
child: Center(child: Text('No hay emisoras disponibles')),
|
||||
);
|
||||
return const SliverFillRemaining(child: Center(child: Text('No hay emisoras disponibles')));
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, i) => TarjetaEmisora(
|
||||
emisora: emisoras[i],
|
||||
onTap: () => context.read<EstadoRadio>().reproducir(emisoras[i]),
|
||||
).animate().fadeIn(delay: (i * 30).ms).slideY(begin: 0.1),
|
||||
childCount: emisoras.length,
|
||||
),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.85,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
return SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, i) => TarjetaEmisora(
|
||||
emisora: emisoras[i],
|
||||
onTap: () => context.read<EstadoRadio>().reproducir(emisoras[i]),
|
||||
).animate().fadeIn(delay: (i * 30).ms).slideY(begin: 0.1),
|
||||
childCount: emisoras.length,
|
||||
),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.85,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user