From 9c51454d570a3a56deff75a0d9d3e10a0b152004 Mon Sep 17 00:00:00 2001 From: freetlab Date: Wed, 20 May 2026 20:07:24 +0200 Subject: [PATCH] fix(ci): resolve premium UI analyzer errors --- lib/app.dart | 142 +++++++++-------- lib/pantallas/pantalla_ajustes.dart | 211 ++++++++++++++++++-------- lib/pantallas/pantalla_buscar.dart | 87 ++++++++--- lib/pantallas/pantalla_favoritos.dart | 25 +-- lib/pantallas/pantalla_inicio.dart | 149 +++++++++++------- lib/widgets/mini_reproductor.dart | 77 +++++++--- lib/widgets/tarjeta_emisora.dart | 116 +++++++++----- pubspec.lock | 14 +- pubspec.yaml | 1 + 9 files changed, 535 insertions(+), 287 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 0a6a50a..3ea10f1 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -7,8 +7,8 @@ import 'pantallas/pantalla_buscar.dart'; import 'pantallas/pantalla_favoritos.dart'; import 'pantallas/pantalla_ajustes.dart'; import 'tema/pluriwave_theme.dart'; -import 'widgets/mini_reproductor.dart'; import 'widgets/pluri_icon.dart'; +import 'package:pluriwave/widgets/mini_reproductor.dart'; class PluriWaveApp extends StatelessWidget { const PluriWaveApp({super.key}); @@ -113,18 +113,19 @@ 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), - ), - ], - ), + 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, @@ -144,58 +145,71 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> { final estado = context.read(); showModalBottomSheet( context: context, - builder: (ctx) => SafeArea( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Timer de sueño', style: Theme.of(ctx).textTheme.titleLarge), - const SizedBox(height: 16), - if (estado.timer.activo) - StreamBuilder( - stream: estado.timer.tiempoRestanteStream, - builder: (ctx, snap) { - final t = snap.data ?? Duration.zero; - final h = t.inHours; - final m = t.inMinutes.remainder(60).toString().padLeft(2, '0'); - final s = t.inSeconds.remainder(60).toString().padLeft(2, '0'); - return Column( - children: [ - Text( - '${h > 0 ? "${h}h " : ""}${m}m ${s}s', - style: Theme.of(ctx).textTheme.headlineMedium, - ), - const SizedBox(height: 8), - FilledButton.tonal( - onPressed: () { - estado.cancelarTimer(); - Navigator.pop(ctx); - }, - child: const Text('Cancelar timer'), - ), - ], - ); - }, - ) - else - Wrap( - spacing: 8, - children: [3, 5, 10, 15, 30, 60, 90, 120, 180] - .map((min) => ActionChip( - label: Text('$min min'), - onPressed: () { - estado.iniciarTimer(min); - Navigator.pop(ctx); - }, - )) - .toList(), - ), - ], + builder: + (ctx) => SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Timer de sueño', + style: Theme.of(ctx).textTheme.titleLarge, + ), + const SizedBox(height: 16), + if (estado.timer.activo) + StreamBuilder( + stream: estado.timer.tiempoRestanteStream, + builder: (ctx, snap) { + final t = snap.data ?? Duration.zero; + final h = t.inHours; + final m = t.inMinutes + .remainder(60) + .toString() + .padLeft(2, '0'); + final s = t.inSeconds + .remainder(60) + .toString() + .padLeft(2, '0'); + return Column( + children: [ + Text( + '${h > 0 ? "${h}h " : ""}${m}m ${s}s', + style: Theme.of(ctx).textTheme.headlineMedium, + ), + const SizedBox(height: 8), + FilledButton.tonal( + onPressed: () { + estado.cancelarTimer(); + Navigator.pop(ctx); + }, + child: const Text('Cancelar timer'), + ), + ], + ); + }, + ) + else + Wrap( + spacing: 8, + children: + [3, 5, 10, 15, 30, 60, 90, 120, 180] + .map( + (min) => ActionChip( + label: Text('$min min'), + onPressed: () { + estado.iniciarTimer(min); + Navigator.pop(ctx); + }, + ), + ) + .toList(), + ), + ], + ), + ), ), - ), - ), ); } } diff --git a/lib/pantallas/pantalla_ajustes.dart b/lib/pantallas/pantalla_ajustes.dart index 0d2bfe9..6cbf1e5 100644 --- a/lib/pantallas/pantalla_ajustes.dart +++ b/lib/pantallas/pantalla_ajustes.dart @@ -1,4 +1,4 @@ -import 'dart:convert'; +import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; @@ -47,7 +47,8 @@ class _SeccionEcualizador extends StatelessWidget { builder: (ctx, estado, _) { final disponible = estado.ecualizadorDisponible; final emisoraActual = estado.emisoraActual; - final mostrarModoPorEmisora = emisoraActual != null && estado.emisoraActualEsFavorita; + final mostrarModoPorEmisora = + emisoraActual != null && estado.emisoraActualEsFavorita; final usandoEqPropio = estado.emisoraActualTienePresetPropio; return PluriGlassSurface( @@ -58,7 +59,10 @@ class _SeccionEcualizador extends StatelessWidget { children: [ const Icon(Icons.equalizer), const SizedBox(width: 12), - Text('Ecualizador', style: Theme.of(ctx).textTheme.titleMedium), + Text( + 'Ecualizador', + style: Theme.of(ctx).textTheme.titleMedium, + ), const Spacer(), if (!disponible) const Chip( @@ -78,7 +82,11 @@ class _SeccionEcualizador extends StatelessWidget { : 'Usando EQ principal para ${emisoraActual.nombre}', ), value: usandoEqPropio, - onChanged: (usarPropio) => estado.cambiarModoEcualizadorEmisoraActual(usarPropio: usarPropio), + onChanged: + (usarPropio) => + estado.cambiarModoEcualizadorEmisoraActual( + usarPropio: usarPropio, + ), ), ], const SizedBox(height: 8), @@ -115,7 +123,10 @@ class _SeccionEmisoras extends StatelessWidget { children: [ const Icon(Icons.add_circle_outline), const SizedBox(width: 12), - Text('Emisoras personalizadas', style: Theme.of(context).textTheme.titleMedium), + Text( + 'Emisoras personalizadas', + style: Theme.of(context).textTheme.titleMedium, + ), const Spacer(), TextButton.icon( icon: const Icon(Icons.add), @@ -127,7 +138,10 @@ class _SeccionEmisoras extends StatelessWidget { if (custom.isEmpty) const Padding( padding: EdgeInsets.only(top: 8), - child: Text('No hay emisoras personalizadas.', style: TextStyle(color: Colors.grey)), + child: Text( + 'No hay emisoras personalizadas.', + style: TextStyle(color: Colors.grey), + ), ) else for (final emisora in custom) @@ -135,19 +149,27 @@ class _SeccionEmisoras extends StatelessWidget { contentPadding: EdgeInsets.zero, leading: const Icon(Icons.radio), title: Text(emisora.nombre), - subtitle: Text(emisora.url, maxLines: 1, overflow: TextOverflow.ellipsis), + subtitle: Text( + emisora.url, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.play_arrow), tooltip: 'Reproducir', - onPressed: () => context.read().reproducir(emisora), + onPressed: + () => context.read().reproducir(emisora), ), IconButton( icon: const Icon(Icons.delete_outline), tooltip: 'Eliminar', - onPressed: () => context.read().eliminarEmitoraCustom(emisora.uuid), + onPressed: + () => context + .read() + .eliminarEmitoraCustom(emisora.uuid), ), ], ), @@ -214,12 +236,22 @@ class _FormularioEmisoraState extends State<_FormularioEmisora> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('Añadir emisora', style: Theme.of(context).textTheme.titleLarge), + Text( + 'Añadir emisora', + style: Theme.of(context).textTheme.titleLarge, + ), const SizedBox(height: 16), TextFormField( controller: _nombreCtrl, - decoration: const InputDecoration(labelText: 'Nombre *', border: OutlineInputBorder()), - validator: (v) => v == null || v.trim().isEmpty ? 'Campo obligatorio' : null, + decoration: const InputDecoration( + labelText: 'Nombre *', + border: OutlineInputBorder(), + ), + validator: + (v) => + v == null || v.trim().isEmpty + ? 'Campo obligatorio' + : null, ), const SizedBox(height: 12), TextFormField( @@ -240,14 +272,22 @@ class _FormularioEmisoraState extends State<_FormularioEmisora> { const SizedBox(height: 12), TextFormField( controller: _paisCtrl, - decoration: const InputDecoration(labelText: 'País (opcional)', border: OutlineInputBorder()), + decoration: const InputDecoration( + labelText: 'País (opcional)', + border: OutlineInputBorder(), + ), ), const SizedBox(height: 20), FilledButton( onPressed: _guardando ? null : _guardar, - child: _guardando - ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2)) - : const Text('Guardar emisora'), + child: + _guardando + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Guardar emisora'), ), ], ), @@ -272,44 +312,68 @@ class _SeccionBackup extends StatelessWidget { await Share.shareXFiles( [XFile(file.path)], subject: 'PluriWave — copia de seguridad', - text: 'Configuración de PluriWave exportada el ${DateTime.now().toLocal()}', + text: + 'Configuración de PluriWave exportada el ${DateTime.now().toLocal()}', ); } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error al exportar: $e'))); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error al exportar: $e'))); } } } Future _importar(BuildContext context) async { try { - final result = await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']); + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['json'], + ); if (result == null || result.files.single.path == null) return; final file = File(result.files.single.path!); - final json = jsonDecode(await file.readAsString()) as Map; + final json = + jsonDecode(await file.readAsString()) as Map; if (context.mounted) { final confirmar = await showDialog( context: context, - builder: (ctx) => AlertDialog( - title: const Text('Importar configuración'), - content: const Text('Esto añadirá los favoritos, emisoras y presets del fichero. ¿Continuar?'), - actions: [ - TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancelar')), - FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Importar')), - ], - ), + builder: + (ctx) => AlertDialog( + title: const Text('Importar configuración'), + content: const Text( + 'Esto añadirá los favoritos, emisoras y presets del fichero. ¿Continuar?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Cancelar'), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Importar'), + ), + ], + ), ); if (confirmar != true) return; if (context.mounted) { - await context.read().importarConfig(json); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Configuración importada correctamente'))); + final estado = context.read(); + final messenger = ScaffoldMessenger.of(context); + await estado.importarConfig(json); + messenger.showSnackBar( + const SnackBar( + content: Text('Configuración importada correctamente'), + ), + ); } } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error al importar: $e'))); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error al importar: $e'))); } } } @@ -324,7 +388,10 @@ class _SeccionBackup extends StatelessWidget { children: [ const Icon(Icons.backup_outlined), const SizedBox(width: 12), - Text('Copia de seguridad', style: Theme.of(context).textTheme.titleMedium), + Text( + 'Copia de seguridad', + style: Theme.of(context).textTheme.titleMedium, + ), ], ), ListTile( @@ -338,7 +405,9 @@ class _SeccionBackup extends StatelessWidget { contentPadding: EdgeInsets.zero, leading: const Icon(Icons.download_outlined), title: const Text('Importar configuración'), - subtitle: const Text('Restaurar desde un fichero de copia de seguridad'), + subtitle: const Text( + 'Restaurar desde un fichero de copia de seguridad', + ), onTap: () => _importar(context), ), ], @@ -353,41 +422,49 @@ class _SeccionInfo extends StatelessWidget { @override Widget build(BuildContext context) { return Consumer( - builder: (ctx, estado, _) => PluriGlassSurface( - child: Column( - children: [ - const ListTile( - contentPadding: EdgeInsets.zero, - leading: PluriIcon(glyph: PluriIconGlyph.settings, variant: PluriIconVariant.filled), - title: Text('PluriWave'), - subtitle: Text('v0.3.0 — Radio mundial'), + builder: + (ctx, estado, _) => PluriGlassSurface( + child: Column( + children: [ + const ListTile( + contentPadding: EdgeInsets.zero, + leading: PluriIcon( + glyph: PluriIconGlyph.settings, + variant: PluriIconVariant.filled, + ), + title: Text('PluriWave'), + subtitle: Text('v0.3.0 — Radio mundial'), + ), + FutureBuilder( + future: estado.favoritos.obtenerTodos().then((l) => l.length), + builder: + (ctx, snap) => ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.favorite_outline), + title: const Text('Favoritos guardados'), + trailing: Text( + snap.data?.toString() ?? '—', + style: Theme.of(ctx).textTheme.bodyLarge, + ), + ), + ), + const ListTile( + contentPadding: EdgeInsets.zero, + leading: Icon(Icons.verified_outlined), + title: Text('Filtro de emisoras'), + subtitle: Text('Solo emisoras verificadas como activas'), + trailing: Icon(Icons.check_circle, color: Colors.green), + ), + const ListTile( + contentPadding: EdgeInsets.zero, + leading: Icon(Icons.music_note_outlined), + title: Text('Audio en background'), + subtitle: Text('Continúa al apagar la pantalla'), + trailing: Icon(Icons.check_circle, color: Colors.green), + ), + ], ), - FutureBuilder( - future: estado.favoritos.obtenerTodos().then((l) => l.length), - builder: (ctx, snap) => ListTile( - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.favorite_outline), - title: const Text('Favoritos guardados'), - trailing: Text(snap.data?.toString() ?? '—', style: Theme.of(ctx).textTheme.bodyLarge), - ), - ), - const ListTile( - contentPadding: EdgeInsets.zero, - leading: Icon(Icons.verified_outlined), - title: Text('Filtro de emisoras'), - subtitle: Text('Solo emisoras verificadas como activas'), - trailing: Icon(Icons.check_circle, color: Colors.green), - ), - const ListTile( - contentPadding: EdgeInsets.zero, - leading: Icon(Icons.music_note_outlined), - title: Text('Audio en background'), - subtitle: Text('Continúa al apagar la pantalla'), - trailing: Icon(Icons.check_circle, color: Colors.green), - ), - ], - ), - ), + ), ); } } diff --git a/lib/pantallas/pantalla_buscar.dart b/lib/pantallas/pantalla_buscar.dart index 7b3f3ca..8a02514 100644 --- a/lib/pantallas/pantalla_buscar.dart +++ b/lib/pantallas/pantalla_buscar.dart @@ -1,22 +1,35 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:provider/provider.dart'; 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'; +import 'package:pluriwave/widgets/tarjeta_emisora.dart'; const _paises = [ - ('España', 'ES'), ('USA', 'US'), ('México', 'MX'), ('Argentina', 'AR'), - ('UK', 'GB'), ('Francia', 'FR'), ('Alemania', 'DE'), ('Italia', 'IT'), - ('Brasil', 'BR'), ('Japón', 'JP'), + ('España', 'ES'), + ('USA', 'US'), + ('México', 'MX'), + ('Argentina', 'AR'), + ('UK', 'GB'), + ('Francia', 'FR'), + ('Alemania', 'DE'), + ('Italia', 'IT'), + ('Brasil', 'BR'), + ('Japón', 'JP'), ]; const _idiomas = [ - 'spanish', 'english', 'french', 'german', 'portuguese', - 'italian', 'japanese', 'arabic', 'russian', + 'spanish', + 'english', + 'french', + 'german', + 'portuguese', + 'italian', + 'japanese', + 'arabic', + 'russian', ]; class PantallaBuscar extends StatefulWidget { @@ -60,7 +73,10 @@ class _PantallaBuscarState extends State { child: SearchBar( controller: _controller, hintText: 'Nombre de la emisora...', - leading: const PluriIcon(glyph: PluriIconGlyph.search, variant: PluriIconVariant.filled), + leading: const PluriIcon( + glyph: PluriIconGlyph.search, + variant: PluriIconVariant.filled, + ), trailing: [ if (_controller.text.isNotEmpty) IconButton( @@ -77,14 +93,24 @@ class _PantallaBuscarState extends State { ), ), ), - _seccionFiltro('País', _paises.map((p) => (p.$1, p.$2)).toList(), _paisSeleccionado, (v) { - setState(() => _paisSeleccionado = v); - _buscar(); - }), - _seccionFiltro('Idioma', _idiomas.map((i) => (i, i)).toList(), _idiomaSeleccionado, (v) { - setState(() => _idiomaSeleccionado = v); - _buscar(); - }), + _seccionFiltro( + 'País', + _paises.map((p) => (p.$1, p.$2)).toList(), + _paisSeleccionado, + (v) { + setState(() => _paisSeleccionado = v); + _buscar(); + }, + ), + _seccionFiltro( + 'Idioma', + _idiomas.map((i) => (i, i)).toList(), + _idiomaSeleccionado, + (v) { + setState(() => _idiomaSeleccionado = v); + _buscar(); + }, + ), Expanded(child: _resultados(estado, theme)), ], ); @@ -138,15 +164,25 @@ class _PantallaBuscarState extends State { final resultados = estado.resultadosBusqueda; if (resultados.isEmpty) { - final sinFiltros = _controller.text.isEmpty && _paisSeleccionado == null && _idiomaSeleccionado == null; + final sinFiltros = + _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 PluriIcon( + glyph: PluriIconGlyph.search, + variant: PluriIconVariant.activeGlow, + size: 44, + ), const SizedBox(height: 14), - Text(sinFiltros ? 'Buscá una emisora' : 'Sin resultados', style: theme.textTheme.titleMedium), + Text( + sinFiltros ? 'Buscá una emisora' : 'Sin resultados', + style: theme.textTheme.titleMedium, + ), ], ), ), @@ -157,11 +193,12 @@ class _PantallaBuscarState extends State { padding: const EdgeInsets.all(16), 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), + itemBuilder: + (context, i) => TarjetaEmisora( + emisora: resultados[i], + esCompacta: true, + onTap: () => context.read().reproducir(resultados[i]), + ).animate().fadeIn(delay: (i * 20).ms), ); } } diff --git a/lib/pantallas/pantalla_favoritos.dart b/lib/pantallas/pantalla_favoritos.dart index 34ff653..414ee34 100644 --- a/lib/pantallas/pantalla_favoritos.dart +++ b/lib/pantallas/pantalla_favoritos.dart @@ -1,11 +1,10 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; 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'; +import 'package:pluriwave/widgets/tarjeta_emisora.dart'; class PantallaFavoritos extends StatelessWidget { const PantallaFavoritos({super.key}); @@ -22,13 +21,19 @@ class PantallaFavoritos extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const PluriIcon(glyph: PluriIconGlyph.favorites, variant: PluriIconVariant.activeGlow, size: 52), + 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), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), ), ], ), @@ -38,8 +43,7 @@ class PantallaFavoritos extends StatelessWidget { return ReorderableListView.builder( padding: const EdgeInsets.all(12), - onReorder: (oldIndex, newIndex) async { - if (newIndex > oldIndex) newIndex--; + onReorderItem: (oldIndex, newIndex) async { final emisora = favoritos[oldIndex]; await estado.favoritos.reordenar(emisora.uuid, newIndex); await estado.cargarFavoritos(); @@ -72,13 +76,16 @@ class PantallaFavoritos extends StatelessWidget { await estado.cargarFavoritos(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('${emisora.nombre} eliminada de favoritos')), + SnackBar( + content: Text( + '${emisora.nombre} eliminada de favoritos', + ), + ), ); } }, ), ], - ), ), ), ); diff --git a/lib/pantallas/pantalla_inicio.dart b/lib/pantallas/pantalla_inicio.dart index df00050..f68f7a7 100644 --- a/lib/pantallas/pantalla_inicio.dart +++ b/lib/pantallas/pantalla_inicio.dart @@ -1,4 +1,4 @@ -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; @@ -7,7 +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/tarjeta_emisora.dart'; +import 'package:pluriwave/widgets/tarjeta_emisora.dart'; /// Pantalla principal: emisoras populares y por género. class PantallaInicio extends StatefulWidget { @@ -19,8 +19,18 @@ class PantallaInicio extends StatefulWidget { class _PantallaInicioState extends State { static const _generos = [ - 'pop', 'rock', 'jazz', 'classical', 'electronic', 'news', - 'talk', 'hip-hop', 'country', 'metal', 'reggae', 'latin', + 'pop', + 'rock', + 'jazz', + 'classical', + 'electronic', + 'news', + 'talk', + 'hip-hop', + 'country', + 'metal', + 'reggae', + 'latin', ]; String? _generoSeleccionado; @@ -37,7 +47,8 @@ class _PantallaInicioState extends State { SliverToBoxAdapter(child: _heroHeader(context)), SliverToBoxAdapter(child: _seccionTendencias(estado, theme)), SliverToBoxAdapter(child: _chipGeneros(context, theme)), - if (estado.error != null) SliverToBoxAdapter(child: _errorBanner(estado, theme)), + if (estado.error != null) + SliverToBoxAdapter(child: _errorBanner(estado, theme)), SliverPadding( padding: EdgeInsets.symmetric(horizontal: t.spacingMd), sliver: _gridEmisoras(estado), @@ -51,16 +62,33 @@ class _PantallaInicioState extends State { final t = context.pluriTokens; final theme = Theme.of(context); return Padding( - padding: EdgeInsets.fromLTRB(t.spacingMd, t.spacingSm, t.spacingMd, t.spacingSm), + 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 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)), + Text( + 'PluriWave', + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + Text( + 'Ondas vivas globales', + style: theme.textTheme.titleMedium?.copyWith(color: t.warmCoral), + ), ], ), ), @@ -79,26 +107,31 @@ class _PantallaInicioState extends State { 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().reproducir(e), - ).animate().fadeIn(delay: (i * 50).ms); - }, - ), + 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().reproducir(e), + ).animate().fadeIn(delay: (i * 50).ms); + }, + ), ), ], ), @@ -119,23 +152,24 @@ class _PantallaInicioState extends State { 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().buscar(tag: g); - } else { - context.read().cargarPopulares(); - } - }, - ); - }).toList(), + 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().buscar(tag: g); + } else { + context.read().cargarPopulares(); + } + }, + ); + }).toList(), ), ], ), @@ -153,7 +187,10 @@ class _PantallaInicioState extends State { 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')), + TextButton( + onPressed: estado.cargarPopulares, + child: const Text('Reintentar'), + ), ], ), ), @@ -161,12 +198,20 @@ class _PantallaInicioState extends State { } Widget _gridEmisoras(EstadoRadio estado) { - final emisoras = _generoSeleccionado != null ? estado.resultadosBusqueda : estado.emisorasInicio; - final cargando = estado.cargandoPopulares || (_generoSeleccionado != null && estado.cargandoBusqueda); + 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, @@ -177,7 +222,9 @@ class _PantallaInicioState extends State { } 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 SliverGrid( diff --git a/lib/widgets/mini_reproductor.dart b/lib/widgets/mini_reproductor.dart index b14e007..5ab03ac 100644 --- a/lib/widgets/mini_reproductor.dart +++ b/lib/widgets/mini_reproductor.dart @@ -9,7 +9,7 @@ import 'pluri_glass_surface.dart'; import 'pluri_icon.dart'; import 'visualizador_audio.dart'; -/// Barra inferior persistente con controles básicos de reproducción. +/// Barra inferior persistente con controles básicos de reproducción. /// Toca la barra para abrir PantallaReproductor completa. class MiniReproductor extends StatelessWidget { const MiniReproductor({super.key}); @@ -26,9 +26,17 @@ class MiniReproductor extends StatelessWidget { return SafeArea( top: false, child: Padding( - padding: EdgeInsets.fromLTRB(t.spacingMd, t.spacingSm, t.spacingMd, t.spacingSm), + padding: EdgeInsets.fromLTRB( + t.spacingMd, + t.spacingSm, + t.spacingMd, + t.spacingSm, + ), child: PluriGlassSurface( - padding: EdgeInsets.symmetric(horizontal: t.spacingSm, vertical: t.spacingXs), + padding: EdgeInsets.symmetric( + horizontal: t.spacingSm, + vertical: t.spacingXs, + ), borderRadius: BorderRadius.circular(999), child: Row( children: [ @@ -67,27 +75,37 @@ class MiniReproductor extends StatelessWidget { children: [ Text( emisora.nombre, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w700, - ), + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith(fontWeight: FontWeight.w700), maxLines: 1, overflow: TextOverflow.ellipsis, ), StreamBuilder( stream: estado.estadoStream, builder: (context, snapshot) { - final s = snapshot.data ?? EstadoReproduccion.detenido; - final activo = s == EstadoReproduccion.reproduciendo; + final s = + snapshot.data ?? + EstadoReproduccion.detenido; + final activo = + s == EstadoReproduccion.reproduciendo; return Text( _labelEstado(s), - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: activo - ? t.warmCoral - : Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.7), - fontWeight: activo ? FontWeight.w600 : FontWeight.w400, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: + activo + ? t.warmCoral + : Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.7), + fontWeight: + activo + ? FontWeight.w600 + : FontWeight.w400, ), ); }, @@ -128,16 +146,28 @@ class MiniReproductor extends StatelessWidget { return IconButton( tooltip: 'Reintentar', icon: const Icon(Icons.refresh_rounded), - onPressed: emisoraActual != null ? () => estado.reproducir(emisoraActual) : null, - constraints: const BoxConstraints.tightFor(width: 48, height: 48), + onPressed: + emisoraActual != null + ? () => estado.reproducir(emisoraActual) + : null, + constraints: const BoxConstraints.tightFor( + width: 48, + height: 48, + ), ); } return Semantics( button: true, - label: s == EstadoReproduccion.reproduciendo ? 'Pausar' : 'Reproducir', + label: + s == EstadoReproduccion.reproduciendo + ? 'Pausar' + : 'Reproducir', child: IconButton( - tooltip: s == EstadoReproduccion.reproduciendo ? 'Pausar' : 'Reproducir', + tooltip: + s == EstadoReproduccion.reproduciendo + ? 'Pausar' + : 'Reproducir', icon: Icon( s == EstadoReproduccion.reproduciendo ? Icons.pause_circle_filled_rounded @@ -145,7 +175,10 @@ class MiniReproductor extends StatelessWidget { color: t.electricMagenta, ), onPressed: estado.togglePlay, - constraints: const BoxConstraints.tightFor(width: 48, height: 48), + constraints: const BoxConstraints.tightFor( + width: 48, + height: 48, + ), ), ); }, @@ -162,7 +195,7 @@ class MiniReproductor extends StatelessWidget { EstadoReproduccion.cargando => 'Conectando...', EstadoReproduccion.reproduciendo => 'En directo', EstadoReproduccion.pausado => 'Pausado', - EstadoReproduccion.error => 'Error de conexión', + EstadoReproduccion.error => 'Error de conexión', EstadoReproduccion.detenido => 'Detenido', }; } diff --git a/lib/widgets/tarjeta_emisora.dart b/lib/widgets/tarjeta_emisora.dart index c18428b..2c76148 100644 --- a/lib/widgets/tarjeta_emisora.dart +++ b/lib/widgets/tarjeta_emisora.dart @@ -10,7 +10,7 @@ import 'pluri_glass_surface.dart'; import 'pluri_icon.dart'; /// Tarjeta compacta para mostrar una emisora en listas y grids. -/// Incluye botón de favorito visible en ambos modos. +/// Incluye botón de favorito visible en ambos modos. class TarjetaEmisora extends StatefulWidget { final Emisora emisora; final VoidCallback? onTap; @@ -37,12 +37,16 @@ class _TarjetaEmisoraState extends State { final esFav = await estado.toggleFavorito(widget.emisora); if (mounted) setState(() => _toggling = false); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(esFav - ? '${widget.emisora.nombre} añadida a favoritos' - : '${widget.emisora.nombre} eliminada de favoritos'), - duration: const Duration(seconds: 2), - )); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + esFav + ? '${widget.emisora.nombre} añadida a favoritos' + : '${widget.emisora.nombre} eliminada de favoritos', + ), + duration: const Duration(seconds: 2), + ), + ); } } @@ -54,7 +58,9 @@ class _TarjetaEmisoraState extends State { label: 'Emisora ${widget.emisora.nombre}', child: PluriGlassSurface( padding: EdgeInsets.zero, - borderRadius: BorderRadius.circular(widget.esCompacta ? t.radiusMd : t.radiusLg), + borderRadius: BorderRadius.circular( + widget.esCompacta ? t.radiusMd : t.radiusLg, + ), child: Material( color: Colors.transparent, child: InkWell( @@ -73,12 +79,14 @@ class _TarjetaEmisoraState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - AspectRatio( - aspectRatio: 1, - child: _logo(60), - ), + AspectRatio(aspectRatio: 1, child: _logo(60)), Padding( - padding: EdgeInsets.fromLTRB(t.spacingMd, t.spacingSm, t.spacingMd, t.spacingMd), + padding: EdgeInsets.fromLTRB( + t.spacingMd, + t.spacingSm, + t.spacingMd, + t.spacingMd, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -96,10 +104,9 @@ class _TarjetaEmisoraState extends State { child: Text( widget.emisora.pais!, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.72), + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.72), ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -121,12 +128,16 @@ class _TarjetaEmisoraState extends State { Widget _buildCompacta() { final t = context.pluriTokens; - final subtitulo = [widget.emisora.pais, widget.emisora.idioma] - .where((s) => s != null && s.isNotEmpty) - .join(' · '); + final subtitulo = [ + widget.emisora.pais, + widget.emisora.idioma, + ].where((s) => s != null && s.isNotEmpty).join(' · '); return Padding( - padding: EdgeInsets.symmetric(horizontal: t.spacingSm, vertical: t.spacingXs), + padding: EdgeInsets.symmetric( + horizontal: t.spacingSm, + vertical: t.spacingXs, + ), child: Row( children: [ ClipRRect( @@ -141,7 +152,9 @@ class _TarjetaEmisoraState extends State { children: [ Text( widget.emisora.nombre, - style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700), + style: Theme.of( + context, + ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -150,10 +163,11 @@ class _TarjetaEmisoraState extends State { subtitulo, maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.72)), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.72), + ), ), ], ), @@ -167,26 +181,37 @@ class _TarjetaEmisoraState extends State { Widget _botonFavorito({required bool mini}) { final t = context.pluriTokens; final esFavorito = context.select( - (estado) => estado.listaFavoritos.any((e) => e.uuid == widget.emisora.uuid), + (estado) => + estado.listaFavoritos.any((e) => e.uuid == widget.emisora.uuid), ); - final icono = mini - ? Icon( - esFavorito ? Icons.favorite_rounded : Icons.favorite_outline_rounded, - color: esFavorito ? t.warmCoral : Colors.white.withValues(alpha: 0.82), - size: 18, - ) - : PluriIcon( - glyph: PluriIconGlyph.favorites, - variant: esFavorito ? PluriIconVariant.activeGlow : PluriIconVariant.outline, - size: 20, - semanticLabel: esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos', - ); + final icono = + mini + ? Icon( + esFavorito + ? Icons.favorite_rounded + : Icons.favorite_outline_rounded, + color: + esFavorito + ? t.warmCoral + : Colors.white.withValues(alpha: 0.82), + size: 18, + ) + : PluriIcon( + glyph: PluriIconGlyph.favorites, + variant: + esFavorito + ? PluriIconVariant.activeGlow + : PluriIconVariant.outline, + size: 20, + semanticLabel: + esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos', + ); return Semantics( button: true, toggled: esFavorito, - label: esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos', + label: esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos', child: Material( color: mini ? t.glassSurface : Colors.transparent, shape: const CircleBorder(), @@ -264,9 +289,16 @@ class TarjetaEmisoraShimmer extends StatelessWidget { child: Container(color: theme.colorScheme.surfaceContainerHighest), ), const SizedBox(height: 8), - Container(height: 14, color: theme.colorScheme.surfaceContainerHighest), + Container( + height: 14, + color: theme.colorScheme.surfaceContainerHighest, + ), const SizedBox(height: 4), - Container(height: 12, width: 60, color: theme.colorScheme.surfaceContainerHighest), + Container( + height: 12, + width: 60, + color: theme.colorScheme.surfaceContainerHighest, + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 2a1b4c7..c2466b8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -353,10 +353,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -369,10 +369,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -414,7 +414,7 @@ packages: source: hosted version: "2.1.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" @@ -686,10 +686,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.11" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 401a522..964937e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: http: ^1.2.2 # Utils + path: ^1.9.1 share_plus: ^10.1.3 file_picker: ^8.1.7 uuid: ^4.5.1