diff --git a/assets/mockups/pluriwave-award-mockup.png b/assets/mockups/pluriwave-award-mockup.png new file mode 100644 index 0000000..852be3b Binary files /dev/null and b/assets/mockups/pluriwave-award-mockup.png differ diff --git a/assets/mockups/pluriwave-award-mockup.prompt.md b/assets/mockups/pluriwave-award-mockup.prompt.md new file mode 100644 index 0000000..03fcc85 --- /dev/null +++ b/assets/mockups/pluriwave-award-mockup.prompt.md @@ -0,0 +1,5 @@ +# PluriWave award mockup prompt + +Generated with built-in image_gen as the visual target for the premium redesign. + +Focus: five mobile screens, dark aurora glassmorphism, cyan/violet/magenta gradients, premium iconography, accessible hierarchy, Home/Search/Favorites/Now Playing/Settings. diff --git a/lib/app.dart b/lib/app.dart index 3ea10f1..37f85f6 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -7,7 +7,9 @@ import 'pantallas/pantalla_buscar.dart'; import 'pantallas/pantalla_favoritos.dart'; import 'pantallas/pantalla_ajustes.dart'; import 'tema/pluriwave_theme.dart'; +import 'widgets/pluri_glass_surface.dart'; import 'widgets/pluri_icon.dart'; +import 'widgets/pluri_wave_scaffold.dart'; import 'package:pluriwave/widgets/mini_reproductor.dart'; class PluriWaveApp extends StatelessWidget { @@ -112,32 +114,40 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> { @override Widget build(BuildContext context) { - return Scaffold( - appBar: - _indice == 3 - ? null // PantallaAjustes tiene su propio AppBar - : AppBar( - title: const Text('PluriWave'), - actions: [ - IconButton( - icon: const Icon(Icons.bedtime_outlined), - tooltip: 'Timer de sueño', - onPressed: () => _mostrarTimerDialog(context), - ), - ], - ), - body: _paginas[_indice], - bottomNavigationBar: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const MiniReproductor(), - NavigationBar( - selectedIndex: _indice, - onDestinationSelected: (i) => setState(() => _indice = i), - destinations: _destinos, + return PluriWaveScaffold( + appBar: AppBar( + title: const Text('PluriWave'), + actions: [ + IconButton( + icon: const Icon(Icons.bedtime_outlined), + tooltip: 'Timer de sueno', + onPressed: () => _mostrarTimerDialog(context), ), ], ), + body: SafeArea(top: false, child: _paginas[_indice]), + bottomNavigationBar: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const MiniReproductor(), + PluriGlassSurface( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + borderRadius: BorderRadius.circular(999), + child: NavigationBar( + selectedIndex: _indice, + height: 66, + onDestinationSelected: (i) => setState(() => _indice = i), + destinations: _destinos, + ), + ), + ], + ), + ), + ), ); } diff --git a/lib/pantallas/pantalla_ajustes.dart b/lib/pantallas/pantalla_ajustes.dart index 6cbf1e5..f4bd8da 100644 --- a/lib/pantallas/pantalla_ajustes.dart +++ b/lib/pantallas/pantalla_ajustes.dart @@ -13,31 +13,54 @@ import '../modelos/emisora.dart'; import '../widgets/ecualizador_widget.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_icon.dart'; -import '../widgets/pluri_wave_scaffold.dart'; +import '../widgets/pluri_premium_widgets.dart'; class PantallaAjustes extends StatelessWidget { const PantallaAjustes({super.key}); @override Widget build(BuildContext context) { - return PluriWaveScaffold( - appBar: AppBar(title: const Text('Ajustes')), - body: ListView( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), - children: const [ - _SeccionEcualizador(), - SizedBox(height: 12), - _SeccionEmisoras(), - SizedBox(height: 12), - _SeccionBackup(), - SizedBox(height: 12), - _SeccionInfo(), - ], - ), + return ListView( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 124), + children: const [ + PluriScreenHeader( + title: 'Ajustes', + subtitle: 'Control fino de sonido, copias de seguridad y emisoras personalizadas.', + glyph: PluriIconGlyph.settings, + trailing: PluriStatusPill( + icon: Icons.security_rounded, + label: 'Seguro', + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: _AjustesContent(), + ), + ], ); } } +class _AjustesContent extends StatelessWidget { + const _AjustesContent(); + + @override + Widget build(BuildContext context) { + return Column( + children: const [ + _SeccionEcualizador(), + SizedBox(height: 12), + _SeccionEmisoras(), + SizedBox(height: 12), + _SeccionBackup(), + SizedBox(height: 12), + _SeccionInfo(), + ], + ); + } +} + + class _SeccionEcualizador extends StatelessWidget { const _SeccionEcualizador(); diff --git a/lib/pantallas/pantalla_buscar.dart b/lib/pantallas/pantalla_buscar.dart index 8a02514..6f498d4 100644 --- a/lib/pantallas/pantalla_buscar.dart +++ b/lib/pantallas/pantalla_buscar.dart @@ -5,19 +5,20 @@ import 'package:provider/provider.dart'; import '../estado/estado_radio.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_icon.dart'; +import '../widgets/pluri_premium_widgets.dart'; import 'package:pluriwave/widgets/tarjeta_emisora.dart'; const _paises = [ - ('España', 'ES'), + ('Espana', 'ES'), ('USA', 'US'), - ('México', 'MX'), + ('Mexico', 'MX'), ('Argentina', 'AR'), ('UK', 'GB'), ('Francia', 'FR'), ('Alemania', 'DE'), ('Italia', 'IT'), ('Brasil', 'BR'), - ('Japón', 'JP'), + ('Japon', 'JP'), ]; const _idiomas = [ @@ -66,13 +67,23 @@ class _PantallaBuscarState extends State { return Column( children: [ + PluriScreenHeader( + title: 'Buscar senal', + subtitle: 'Encontra radios por nombre, pais o idioma con filtros rapidos y alto contraste.', + glyph: PluriIconGlyph.search, + trailing: const PluriStatusPill( + icon: Icons.tune_rounded, + label: 'Filtros', + ), + ), Padding( padding: const EdgeInsets.fromLTRB(16, 10, 16, 0), child: PluriGlassSurface( padding: const EdgeInsets.all(10), + borderRadius: BorderRadius.circular(999), child: SearchBar( controller: _controller, - hintText: 'Nombre de la emisora...', + hintText: 'Radio Horizonte, jazz, noticias...', leading: const PluriIcon( glyph: PluriIconGlyph.search, variant: PluriIconVariant.filled, @@ -94,7 +105,7 @@ class _PantallaBuscarState extends State { ), ), _seccionFiltro( - 'País', + 'Pais', _paises.map((p) => (p.$1, p.$2)).toList(), _paisSeleccionado, (v) { @@ -130,14 +141,19 @@ class _PantallaBuscarState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(titulo, style: theme.textTheme.labelLarge), - const SizedBox(height: 4), + Text( + titulo, + style: theme.textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 6), SizedBox( - height: 36, + height: 40, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: opciones.length, - separatorBuilder: (_, __) => const SizedBox(width: 6), + separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (_, i) { final (label, value) = opciones[i]; final sel = seleccionado == value; @@ -168,37 +184,24 @@ class _PantallaBuscarState extends State { _controller.text.isEmpty && _paisSeleccionado == null && _idiomaSeleccionado == null; - return Center( - child: PluriGlassSurface( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const PluriIcon( - glyph: PluriIconGlyph.search, - variant: PluriIconVariant.activeGlow, - size: 44, - ), - const SizedBox(height: 14), - Text( - sinFiltros ? 'Buscá una emisora' : 'Sin resultados', - style: theme.textTheme.titleMedium, - ), - ], - ), - ), + return PluriEmptyState( + glyph: PluriIconGlyph.search, + title: sinFiltros ? 'Busca una emisora' : 'Sin resultados', + subtitle: sinFiltros + ? 'Usa la barra superior o los chips para descubrir senales de todo el mundo.' + : 'Proba quitar filtros o escribir otro nombre para encontrar una senal activa.', ); } return ListView.separated( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(16, 16, 16, 120), itemCount: resultados.length, - separatorBuilder: (_, __) => const SizedBox(height: 6), - itemBuilder: - (context, i) => TarjetaEmisora( - emisora: resultados[i], - esCompacta: true, - onTap: () => context.read().reproducir(resultados[i]), - ).animate().fadeIn(delay: (i * 20).ms), + separatorBuilder: (_, __) => const SizedBox(height: 10), + itemBuilder: (context, i) => TarjetaEmisora( + emisora: resultados[i], + esCompacta: true, + onTap: () => context.read().reproducir(resultados[i]), + ).animate().fadeIn(delay: (i * 20).ms).slideY(begin: 0.08), ); } } diff --git a/lib/pantallas/pantalla_favoritos.dart b/lib/pantallas/pantalla_favoritos.dart index 837084f..df8334a 100644 --- a/lib/pantallas/pantalla_favoritos.dart +++ b/lib/pantallas/pantalla_favoritos.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import '../estado/estado_radio.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_icon.dart'; +import '../widgets/pluri_premium_widgets.dart'; import 'package:pluriwave/widgets/tarjeta_emisora.dart'; class PantallaFavoritos extends StatelessWidget { @@ -13,84 +14,102 @@ class PantallaFavoritos extends StatelessWidget { Widget build(BuildContext context) { final estado = context.watch(); final favoritos = estado.listaFavoritos; - final theme = Theme.of(context); if (favoritos.isEmpty) { - return Center( - child: PluriGlassSurface( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const PluriIcon( - glyph: PluriIconGlyph.favorites, - variant: PluriIconVariant.activeGlow, - size: 52, - ), - const SizedBox(height: 16), - Text('Sin favoritos aún', style: theme.textTheme.titleMedium), - const SizedBox(height: 8), - Text( - 'Tocá ♥ en cualquier emisora para guardarla', - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], + return const Column( + children: [ + PluriScreenHeader( + title: 'Favoritos', + subtitle: 'Tu cabina personal para volver a las senales que mas escuchas.', + glyph: PluriIconGlyph.favorites, + trailing: PluriStatusPill( + icon: Icons.favorite_rounded, + label: 'Coleccion', + ), ), - ), + Expanded( + child: PluriEmptyState( + glyph: PluriIconGlyph.favorites, + title: 'Sin favoritos aun', + subtitle: 'Toca el corazon en cualquier emisora para guardarla en tu coleccion.', + ), + ), + ], ); } - return ReorderableListView.builder( - padding: const EdgeInsets.all(12), - onReorder: (oldIndex, newIndex) async { - if (newIndex > oldIndex) newIndex--; - final emisora = favoritos[oldIndex]; - await estado.favoritos.reordenar(emisora.uuid, newIndex); - await estado.cargarFavoritos(); - }, - itemCount: favoritos.length, - itemBuilder: (context, i) { - final emisora = favoritos[i]; - return Padding( - key: ValueKey('favorito-pad-${emisora.uuid}'), - padding: const EdgeInsets.symmetric(vertical: 4), - child: PluriGlassSurface( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - child: Row( - children: [ - const Icon(Icons.drag_indicator_rounded), - const SizedBox(width: 6), - Expanded( - child: TarjetaEmisora( - key: Key(emisora.uuid), - emisora: emisora, - esCompacta: true, - onTap: () => estado.reproducir(emisora), + return Column( + children: [ + PluriScreenHeader( + title: 'Favoritos', + subtitle: 'Reordena tu coleccion y deja arriba las radios que mas importan.', + glyph: PluriIconGlyph.favorites, + trailing: PluriStatusPill( + icon: Icons.library_music_rounded, + label: '${favoritos.length} guardadas', + ), + ), + Expanded( + child: ReorderableListView.builder( + padding: const EdgeInsets.fromLTRB(12, 4, 12, 122), + proxyDecorator: (child, index, animation) => ScaleTransition( + scale: Tween(begin: 1, end: 1.03).animate(animation), + child: child, + ), + onReorder: (oldIndex, newIndex) async { + if (newIndex > oldIndex) newIndex--; + final emisora = favoritos[oldIndex]; + await estado.favoritos.reordenar(emisora.uuid, newIndex); + await estado.cargarFavoritos(); + }, + itemCount: favoritos.length, + itemBuilder: (context, i) { + final emisora = favoritos[i]; + return Padding( + key: ValueKey('favorito-pad-${emisora.uuid}'), + padding: const EdgeInsets.symmetric(vertical: 5), + child: PluriGlassSurface( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 7), + child: Row( + children: [ + ReorderableDragStartListener( + index: i, + child: const Padding( + padding: EdgeInsets.all(8), + child: Icon(Icons.drag_indicator_rounded), + ), + ), + Expanded( + child: TarjetaEmisora( + key: Key(emisora.uuid), + emisora: emisora, + esCompacta: true, + onTap: () => estado.reproducir(emisora), + ), + ), + IconButton.filledTonal( + tooltip: 'Eliminar de favoritos', + icon: const Icon(Icons.delete_outline_rounded), + onPressed: () async { + await estado.favoritos.eliminar(emisora.uuid); + await estado.cargarFavoritos(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${emisora.nombre} eliminada de favoritos'), + ), + ); + } + }, + ), + ], ), ), - IconButton( - tooltip: 'Eliminar de favoritos', - icon: const Icon(Icons.delete_outline_rounded), - onPressed: () async { - await estado.favoritos.eliminar(emisora.uuid); - await estado.cargarFavoritos(); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - '${emisora.nombre} eliminada de favoritos', - ), - ), - ); - } - }, - ), - ], - ), + ); + }, ), - ); - }, + ), + ], ); } } diff --git a/lib/pantallas/pantalla_inicio.dart b/lib/pantallas/pantalla_inicio.dart index f68f7a7..a04275b 100644 --- a/lib/pantallas/pantalla_inicio.dart +++ b/lib/pantallas/pantalla_inicio.dart @@ -7,6 +7,7 @@ import '../estado/estado_radio.dart'; import '../tema/pluriwave_theme.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_icon.dart'; +import '../widgets/pluri_premium_widgets.dart'; import 'package:pluriwave/widgets/tarjeta_emisora.dart'; /// Pantalla principal: emisoras populares y por género. @@ -44,13 +45,13 @@ class _PantallaInicioState extends State { onRefresh: estado.cargarPopulares, child: CustomScrollView( slivers: [ - SliverToBoxAdapter(child: _heroHeader(context)), + SliverToBoxAdapter(child: _heroHeader(context, estado)), 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), + padding: EdgeInsets.fromLTRB(t.spacingMd, 0, t.spacingMd, 124), sliver: _gridEmisoras(estado), ), ], @@ -58,39 +59,27 @@ class _PantallaInicioState extends State { ); } - 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 _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', + ), + ], ), ); } @@ -103,7 +92,7 @@ class _PantallaInicioState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Tendencias premium', style: theme.textTheme.titleMedium), + Text('Radar en directo', style: theme.textTheme.titleMedium), const SizedBox(height: 8), SizedBox( height: 56, @@ -214,16 +203,20 @@ class _PantallaInicioState extends State { ), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, - childAspectRatio: 0.85, - crossAxisSpacing: 8, - mainAxisSpacing: 8, + childAspectRatio: 0.78, + crossAxisSpacing: 12, + mainAxisSpacing: 12, ), ); } if (emisoras.isEmpty) { return const SliverFillRemaining( - child: Center(child: Text('No hay emisoras disponibles')), + child: PluriEmptyState( + glyph: PluriIconGlyph.home, + title: 'No hay emisoras disponibles', + subtitle: 'Proba refrescar o elegir otro g?nero para volver a capturar se?al.', + ), ); } @@ -237,9 +230,9 @@ class _PantallaInicioState extends State { ), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, - childAspectRatio: 0.85, - crossAxisSpacing: 8, - mainAxisSpacing: 8, + childAspectRatio: 0.78, + crossAxisSpacing: 12, + mainAxisSpacing: 12, ), ); } diff --git a/lib/tema/pluriwave_theme.dart b/lib/tema/pluriwave_theme.dart index 33e5fc8..fed63de 100644 --- a/lib/tema/pluriwave_theme.dart +++ b/lib/tema/pluriwave_theme.dart @@ -9,10 +9,12 @@ abstract final class PluriWaveTheme { const tokens = PluriWaveTokens.dark; final colorScheme = const ColorScheme.dark().copyWith( primary: tokens.electricMagenta, - secondary: tokens.warmCoral, - surface: const Color(0xFF130B22), - surfaceContainerLow: const Color(0xFF1A112C), - onSurface: const Color(0xFFF4EEFF), + secondary: const Color(0xFF20E6FF), + tertiary: tokens.warmCoral, + surface: const Color(0xFF0B1024), + surfaceContainerLow: const Color(0xFF111831), + surfaceContainerHighest: const Color(0xFF202946), + onSurface: const Color(0xFFF7F2FF), onPrimary: Colors.white, ); @@ -20,11 +22,34 @@ abstract final class PluriWaveTheme { useMaterial3: true, colorScheme: colorScheme, scaffoldBackgroundColor: tokens.deepViolet, - textTheme: GoogleFonts.interTextTheme(ThemeData.dark().textTheme), + textTheme: GoogleFonts.plusJakartaSansTextTheme(ThemeData.dark().textTheme), extensions: const >[ tokens, PluriWaveMotion.dark, ], + appBarTheme: const AppBarTheme( + centerTitle: false, + backgroundColor: Colors.transparent, + foregroundColor: Color(0xFFF7F2FF), + elevation: 0, + scrolledUnderElevation: 0, + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: Colors.transparent, + indicatorColor: tokens.electricMagenta.withValues(alpha: 0.18), + labelTextStyle: WidgetStateProperty.resolveWith( + (states) => TextStyle( + fontWeight: states.contains(WidgetState.selected) ? FontWeight.w800 : FontWeight.w600, + fontSize: 12, + ), + ), + ), + chipTheme: ChipThemeData( + backgroundColor: const Color(0x1FFFFFFF), + selectedColor: tokens.electricMagenta.withValues(alpha: 0.24), + side: BorderSide(color: tokens.glassBorder), + labelStyle: const TextStyle(fontWeight: FontWeight.w700), + ), cardTheme: CardThemeData( elevation: 0, color: colorScheme.surfaceContainerLow, diff --git a/lib/tema/pluriwave_tokens.dart b/lib/tema/pluriwave_tokens.dart index cb55536..01ffc7c 100644 --- a/lib/tema/pluriwave_tokens.dart +++ b/lib/tema/pluriwave_tokens.dart @@ -37,15 +37,15 @@ class PluriWaveTokens extends ThemeExtension { final double spacingLg; static const dark = PluriWaveTokens( - deepViolet: Color(0xFF24123D), - electricMagenta: Color(0xFFE228D1), - warmCoral: Color(0xFFFF6F61), - glassSurface: Color(0x2AFFFFFF), - glassBorder: Color(0x40FFFFFF), - glowColor: Color(0x66E228D1), - radiusSm: 10, - radiusMd: 16, - radiusLg: 24, + deepViolet: Color(0xFF070A18), + electricMagenta: Color(0xFFFF3DF2), + warmCoral: Color(0xFFFFB86B), + glassSurface: Color(0x24FFFFFF), + glassBorder: Color(0x52FFFFFF), + glowColor: Color(0x88FF3DF2), + radiusSm: 14, + radiusMd: 22, + radiusLg: 30, spacingXs: 4, spacingSm: 8, spacingMd: 16, diff --git a/lib/widgets/pluri_glass_surface.dart b/lib/widgets/pluri_glass_surface.dart index e6c1180..6465a2e 100644 --- a/lib/widgets/pluri_glass_surface.dart +++ b/lib/widgets/pluri_glass_surface.dart @@ -10,13 +10,15 @@ class PluriGlassSurface extends StatelessWidget { required this.child, this.padding = const EdgeInsets.all(16), this.borderRadius, - this.blurSigma = 14, + this.blurSigma = 18, + this.glowColor, }); final Widget child; final EdgeInsetsGeometry padding; final BorderRadius? borderRadius; final double blurSigma; + final Color? glowColor; @override Widget build(BuildContext context) { @@ -32,6 +34,14 @@ class PluriGlassSurface extends StatelessWidget { color: t.glassSurface, borderRadius: radius, border: Border.all(color: t.glassBorder), + boxShadow: [ + BoxShadow( + color: glowColor ?? t.glowColor.withValues(alpha: 0.12), + blurRadius: 30, + spreadRadius: -14, + offset: const Offset(0, 18), + ), + ], ), child: Padding(padding: padding, child: child), ), diff --git a/lib/widgets/pluri_premium_widgets.dart b/lib/widgets/pluri_premium_widgets.dart new file mode 100644 index 0000000..38e158f --- /dev/null +++ b/lib/widgets/pluri_premium_widgets.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; + +import '../tema/pluriwave_theme.dart'; +import 'pluri_glass_surface.dart'; +import 'pluri_icon.dart'; + +class PluriScreenHeader extends StatelessWidget { + const PluriScreenHeader({ + super.key, + required this.title, + required this.subtitle, + required this.glyph, + this.primaryActionLabel, + this.onPrimaryAction, + this.trailing, + }); + + final String title; + final String subtitle; + final PluriIconGlyph glyph; + final String? primaryActionLabel; + final VoidCallback? onPrimaryAction; + final Widget? trailing; + + @override + Widget build(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 + 8), + padding: const EdgeInsets.all(18), + child: Stack( + children: [ + Positioned( + right: -36, + top: -42, + child: _Orb(color: t.electricMagenta.withValues(alpha: 0.38), size: 128), + ), + Positioned( + right: 44, + bottom: -54, + child: _Orb(color: const Color(0xFF20E6FF).withValues(alpha: 0.22), size: 116), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + const Color(0xFF20E6FF).withValues(alpha: 0.95), + t.electricMagenta, + t.warmCoral, + ], + ), + boxShadow: [ + BoxShadow(color: t.glowColor, blurRadius: 28, spreadRadius: 2), + ], + ), + child: Center( + child: PluriIcon(glyph: glyph, variant: PluriIconVariant.filled, size: 28), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w900, + letterSpacing: -0.7, + height: 1.02, + ), + ), + const SizedBox(height: 6), + Text( + subtitle, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.76), + height: 1.25, + ), + ), + if (primaryActionLabel != null) ...[ + const SizedBox(height: 12), + FilledButton.tonalIcon( + onPressed: onPrimaryAction, + icon: const Icon(Icons.auto_awesome_rounded, size: 18), + label: Text(primaryActionLabel!), + ), + ], + ], + ), + ), + if (trailing != null) trailing!, + ], + ), + ], + ), + ), + ); + } +} + +class PluriStatusPill extends StatelessWidget { + const PluriStatusPill({ + super.key, + required this.icon, + required this.label, + this.accent, + }); + + final IconData icon; + final String label; + final Color? accent; + + @override + Widget build(BuildContext context) { + final t = context.pluriTokens; + final color = accent ?? t.electricMagenta; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999), + color: color.withValues(alpha: 0.13), + border: Border.all(color: color.withValues(alpha: 0.38)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 7), + Text(label, style: Theme.of(context).textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w800)), + ], + ), + ); + } +} + +class PluriEmptyState extends StatelessWidget { + const PluriEmptyState({ + super.key, + required this.glyph, + required this.title, + required this.subtitle, + }); + + final PluriIconGlyph glyph; + final String title; + final String subtitle; + + @override + Widget build(BuildContext context) { + final t = context.pluriTokens; + final theme = Theme.of(context); + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: PluriGlassSurface( + borderRadius: BorderRadius.circular(t.radiusLg + 10), + padding: const EdgeInsets.all(28), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + PluriIcon(glyph: glyph, variant: PluriIconVariant.activeGlow, size: 58), + const SizedBox(height: 18), + Text(title, textAlign: TextAlign.center, style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900)), + const SizedBox(height: 8), + Text( + subtitle, + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurface.withValues(alpha: 0.72)), + ), + ], + ), + ), + ), + ); + } +} + +class _Orb extends StatelessWidget { + const _Orb({required this.color, required this.size}); + + final Color color; + final double size; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient(colors: [color, color.withValues(alpha: 0)]), + ), + ), + ); + } +} diff --git a/lib/widgets/pluri_wave_scaffold.dart b/lib/widgets/pluri_wave_scaffold.dart index bf61fce..5b56d31 100644 --- a/lib/widgets/pluri_wave_scaffold.dart +++ b/lib/widgets/pluri_wave_scaffold.dart @@ -24,20 +24,62 @@ class PluriWaveScaffold extends StatelessWidget { appBar: appBar, bottomNavigationBar: bottomNavigationBar, floatingActionButton: floatingActionButton, + extendBody: true, body: DecoratedBox( decoration: BoxDecoration( - gradient: RadialGradient( - center: const Alignment(-0.75, -0.9), - radius: 1.25, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, colors: [ - t.electricMagenta.withValues(alpha: 0.22), + const Color(0xFF070A18), t.deepViolet, - const Color(0xFF10091B), + const Color(0xFF151033), + const Color(0xFF070A18), ], - stops: const [0.0, 0.42, 1.0], + stops: const [0, 0.34, 0.68, 1], ), ), - child: body, + child: Stack( + fit: StackFit.expand, + children: [ + Positioned( + left: -120, + top: -120, + child: _AuroraOrb(size: 300, color: const Color(0xFF20E6FF).withValues(alpha: 0.32)), + ), + Positioned( + right: -150, + top: 160, + child: _AuroraOrb(size: 340, color: t.electricMagenta.withValues(alpha: 0.26)), + ), + Positioned( + left: -90, + bottom: 80, + child: _AuroraOrb(size: 260, color: t.warmCoral.withValues(alpha: 0.16)), + ), + body, + ], + ), + ), + ); + } +} + +class _AuroraOrb extends StatelessWidget { + const _AuroraOrb({required this.size, required this.color}); + final double size; + final Color color; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient(colors: [color, color.withValues(alpha: 0)]), + ), ), ); } diff --git a/lib/widgets/tarjeta_emisora.dart b/lib/widgets/tarjeta_emisora.dart index 2c76148..fe21889 100644 --- a/lib/widgets/tarjeta_emisora.dart +++ b/lib/widgets/tarjeta_emisora.dart @@ -79,7 +79,32 @@ class _TarjetaEmisoraState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - AspectRatio(aspectRatio: 1, child: _logo(60)), + AspectRatio( + aspectRatio: 1, + child: Stack( + fit: StackFit.expand, + children: [ + _logo(60), + DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withValues(alpha: 0.34), + ], + ), + ), + ), + Positioned( + left: t.spacingSm, + bottom: t.spacingSm, + child: _LiveBadge(mini: true), + ), + ], + ), + ), Padding( padding: EdgeInsets.fromLTRB( t.spacingMd, @@ -140,9 +165,25 @@ class _TarjetaEmisoraState extends State { ), child: Row( children: [ - ClipRRect( - borderRadius: BorderRadius.circular(t.radiusSm), - child: SizedBox(width: 48, height: 48, child: _logo(24)), + Stack( + alignment: Alignment.center, + children: [ + Container( + width: 58, + height: 58, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: SweepGradient( + colors: [t.electricMagenta, const Color(0xFF20E6FF), t.warmCoral, t.electricMagenta], + ), + boxShadow: [BoxShadow(color: t.glowColor.withValues(alpha: 0.24), blurRadius: 22)], + ), + ), + ClipRRect( + borderRadius: BorderRadius.circular(18), + child: SizedBox(width: 50, height: 50, child: _logo(24)), + ), + ], ), SizedBox(width: t.spacingSm), Expanded( @@ -172,6 +213,8 @@ class _TarjetaEmisoraState extends State { ], ), ), + const SizedBox(width: 8), + _LiveBadge(mini: false), _botonFavorito(mini: false), ], ), @@ -271,6 +314,35 @@ class _TarjetaEmisoraState extends State { } } +class _LiveBadge extends StatelessWidget { + const _LiveBadge({required this.mini}); + + final bool mini; + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme.secondary; + return Container( + padding: EdgeInsets.symmetric(horizontal: mini ? 8 : 6, vertical: mini ? 5 : 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999), + color: Colors.black.withValues(alpha: 0.35), + border: Border.all(color: color.withValues(alpha: 0.48)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.fiber_manual_record_rounded, size: mini ? 10 : 8, color: color), + if (mini) ...[ + const SizedBox(width: 5), + Text('Live', style: Theme.of(context).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w900)), + ], + ], + ), + ); + } +} + /// Placeholder shimmer para listas en carga. class TarjetaEmisoraShimmer extends StatelessWidget { const TarjetaEmisoraShimmer({super.key}); diff --git a/pubspec.yaml b/pubspec.yaml index dd691f5..4709407 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,3 +62,4 @@ flutter: assets: - assets/images/ - assets/icons/ + - assets/mockups/