From ac5ab2316f7dfc69aac8b7bb251f1ed1dcd136ab Mon Sep 17 00:00:00 2001 From: "Kira (Agent)" Date: Sat, 4 Apr 2026 18:24:09 +0200 Subject: [PATCH] feat(v0.4.0): PantallaReproductor + PantallaAjustes + MiniReproductor tappable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PantallaReproductor: artwork grande con sombra animada al reproducir, info chips (país/idioma), codec/bitrate, controles play/pause/stop, indicador en vivo, botón favorito toggle, widget timer inline, animaciones entrada (scale + fadeIn + slideY), transición slide-up. - PantallaAjustes: estado sistema (filtro, background), conteo favoritos, preview de features futuras (Export/Import, radio custom, EQ). - MiniReproductor: GestureDetector → abre PantallaReproductor al tap. - app.dart: 4 tabs (Inicio/Buscar/Favoritos/Ajustes), AppBar condicional. --- CHANGELOG.md | 8 + lib/app.dart | 30 +- lib/pantallas/pantalla_ajustes.dart | 84 +++++ lib/pantallas/pantalla_reproductor.dart | 472 ++++++++++++++++++++++++ lib/widgets/mini_reproductor.dart | 178 ++++----- 5 files changed, 677 insertions(+), 95 deletions(-) create mode 100644 lib/pantallas/pantalla_ajustes.dart create mode 100644 lib/pantallas/pantalla_reproductor.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index f3af761..f034b69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog — PluriWave +## [0.4.0] — 2026-04-04 + +### Añadido +- **PantallaReproductor** — pantalla completa del reproductor. Accesible tocando MiniReproductor o cualquier emisora. Incluye: artwork/logo grande con sombra animada al reproducir, nombre + chips info (país, idioma), codec/bitrate, controles play/pause/stop con indicador "en vivo", botón favorito (toggle), widget de timer (iniciar/cancelar desde la pantalla), animación de entrada slide-up. Transición pageRoute desde cualquier pantalla. +- **PantallaAjustes** — pantalla de ajustes básica (tab nuevo en NavigationBar). Muestra estado del sistema (filtro emisoras, audio background), conteo de favoritos, preview de features próximas (Export/Import, radio personalizada, ecualizador). +- **MiniReproductor** — ahora es tappable: toca la barra para abrir PantallaReproductor. +- **NavigationBar** — añadido tab "Ajustes" (4 destinos: Inicio/Buscar/Favoritos/Ajustes). + ## [0.3.0] — 2026-04-04 ### Fixes (prioridad alta — petición WhikY) diff --git a/lib/app.dart b/lib/app.dart index 8f00a8e..41ac6a7 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,6 +5,7 @@ import 'estado/estado_radio.dart'; import 'pantallas/pantalla_inicio.dart'; import 'pantallas/pantalla_buscar.dart'; import 'pantallas/pantalla_favoritos.dart'; +import 'pantallas/pantalla_ajustes.dart'; import 'widgets/mini_reproductor.dart'; class PluriWaveApp extends StatelessWidget { @@ -63,6 +64,7 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> { PantallaInicio(), PantallaBuscar(), PantallaFavoritos(), + PantallaAjustes(), ]; static const _destinos = [ @@ -81,12 +83,16 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> { selectedIcon: Icon(Icons.favorite), label: 'Favoritos', ), + NavigationDestination( + icon: Icon(Icons.settings_outlined), + selectedIcon: Icon(Icons.settings), + label: 'Ajustes', + ), ]; @override void didChangeDependencies() { super.didChangeDependencies(); - // Suscribir al stream de errores → SnackBar flotante context.read().errorStream.listen((msg) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( @@ -102,16 +108,18 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> { @override Widget build(BuildContext context) { return Scaffold( - 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, diff --git a/lib/pantallas/pantalla_ajustes.dart b/lib/pantallas/pantalla_ajustes.dart new file mode 100644 index 0000000..68d1ba8 --- /dev/null +++ b/lib/pantallas/pantalla_ajustes.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../estado/estado_radio.dart'; + +/// Pantalla de ajustes — por ahora muestra info de la app. +/// En Fase 3 se añadirá Export/Import config y gestión PRO. +class PantallaAjustes extends StatelessWidget { + const PantallaAjustes({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final estado = context.read(); + + return Scaffold( + appBar: AppBar(title: const Text('Ajustes')), + body: ListView( + children: [ + // Info app + ListTile( + leading: const Icon(Icons.info_outline), + title: const Text('PluriWave'), + subtitle: const Text('v0.3.0 — Radio mundial'), + ), + const Divider(), + // Favoritos + FutureBuilder( + future: estado.favoritos.obtenerTodos().then((l) => l.length), + builder: (ctx, snap) => ListTile( + leading: const Icon(Icons.favorite_outline), + title: const Text('Favoritos guardados'), + trailing: Text( + snap.data?.toString() ?? '—', + style: theme.textTheme.bodyLarge, + ), + ), + ), + const Divider(), + // Filtro emisoras + ListTile( + leading: const Icon(Icons.verified_outlined), + title: const Text('Filtro de emisoras'), + subtitle: const Text('Solo emisoras verificadas como activas'), + trailing: const Icon(Icons.check_circle, color: Colors.green), + ), + ListTile( + leading: const Icon(Icons.music_off_outlined), + title: const Text('Audio en background'), + subtitle: const Text('Activo — continúa al apagar pantalla'), + trailing: const Icon(Icons.check_circle, color: Colors.green), + ), + const Divider(), + // Próximamente + const Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Text('PRÓXIMAMENTE', style: TextStyle(fontSize: 12, letterSpacing: 1.2)), + ), + ListTile( + leading: const Icon(Icons.upload_outlined), + title: const Text('Exportar configuración'), + subtitle: const Text('Favoritos, radios custom, presets EQ'), + enabled: false, + ), + ListTile( + leading: const Icon(Icons.download_outlined), + title: const Text('Importar configuración'), + enabled: false, + ), + ListTile( + leading: const Icon(Icons.add_circle_outline), + title: const Text('Añadir radio personalizada'), + enabled: false, + ), + ListTile( + leading: const Icon(Icons.equalizer_outlined), + title: const Text('Ecualizador'), + subtitle: const Text('5 bandas, presets por emisora'), + enabled: false, + ), + ], + ), + ); + } +} diff --git a/lib/pantallas/pantalla_reproductor.dart b/lib/pantallas/pantalla_reproductor.dart new file mode 100644 index 0000000..3d9e95b --- /dev/null +++ b/lib/pantallas/pantalla_reproductor.dart @@ -0,0 +1,472 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; +import '../estado/estado_radio.dart'; +import '../modelos/emisora.dart'; +import '../servicios/servicio_audio.dart'; +import '../servicios/servicio_timer.dart'; + +/// Pantalla completa del reproductor de radio. +/// +/// Muestra: carátula/logo grande, nombre emisora, información (país, idioma, +/// codec/bitrate), controles play/pause, botón favorito, acceso al timer. +/// +/// Se abre como ruta desde cualquier pantalla al pulsar sobre una emisora +/// o desde el MiniReproductor. +class PantallaReproductor extends StatefulWidget { + final Emisora emisora; + + const PantallaReproductor({super.key, required this.emisora}); + + /// Navega a la pantalla del reproductor. + static Future abrir(BuildContext context, Emisora emisora) { + return Navigator.push( + context, + PageRouteBuilder( + pageBuilder: (_, animation, __) => PantallaReproductor(emisora: emisora), + transitionsBuilder: (_, animation, __, child) => SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).animate(CurvedAnimation(parent: animation, curve: Curves.easeOutCubic)), + child: child, + ), + transitionDuration: const Duration(milliseconds: 350), + ), + ); + } + + @override + State createState() => _PantallaReproductorState(); +} + +class _PantallaReproductorState extends State + with SingleTickerProviderStateMixin { + late AnimationController _pulseController; + bool _esFavorito = false; + + @override + void initState() { + super.initState(); + _pulseController = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + ); + _checkFavorito(); + _iniciarReproduccion(); + } + + Future _checkFavorito() async { + final estado = context.read(); + final fav = await estado.esFavorito(widget.emisora.uuid); + if (mounted) setState(() => _esFavorito = fav); + } + + Future _iniciarReproduccion() async { + final estado = context.read(); + // Solo reproductor si no es ya la emisora activa + if (estado.emisoraActual?.uuid != widget.emisora.uuid) { + await estado.reproducir(widget.emisora); + } + } + + @override + void dispose() { + _pulseController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final estado = context.watch(); + + return Scaffold( + backgroundColor: theme.colorScheme.surface, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.keyboard_arrow_down_rounded, size: 32), + tooltip: 'Cerrar', + onPressed: () => Navigator.pop(context), + ), + actions: [ + // Botón favorito + IconButton( + icon: Icon( + _esFavorito ? Icons.favorite_rounded : Icons.favorite_outline_rounded, + color: _esFavorito ? theme.colorScheme.error : null, + ), + tooltip: _esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos', + onPressed: () async { + final esFav = await estado.toggleFavorito(widget.emisora); + if (mounted) setState(() => _esFavorito = esFav); + }, + ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + children: [ + const Spacer(flex: 1), + // Carátula / logo grande + _Artwork( + emisora: widget.emisora, + estadoStream: estado.estadoStream, + ).animate().scale(begin: const Offset(0.8, 0.8), duration: 400.ms, + curve: Curves.easeOutBack), + const SizedBox(height: 32), + // Nombre de la emisora + Text( + widget.emisora.nombre, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ).animate().fadeIn(delay: 150.ms), + const SizedBox(height: 8), + // Info: país, idioma + _InfoChips(emisora: widget.emisora) + .animate() + .fadeIn(delay: 200.ms) + .slideY(begin: 0.2), + const SizedBox(height: 4), + // Codec / bitrate + if (widget.emisora.codec != null || widget.emisora.bitrate != null) + Text( + _codecInfo(widget.emisora), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ).animate().fadeIn(delay: 250.ms), + const Spacer(flex: 2), + // Controles + _Controles( + estado: estado, + emisora: widget.emisora, + ).animate().fadeIn(delay: 300.ms).slideY(begin: 0.3), + const SizedBox(height: 24), + // Timer + _TimerWidget(estado: estado) + .animate() + .fadeIn(delay: 400.ms), + const Spacer(flex: 1), + ], + ), + ), + ), + ); + } + + String _codecInfo(Emisora e) { + final parts = []; + if (e.codec != null) parts.add(e.codec!.toUpperCase()); + if (e.bitrate != null && e.bitrate! > 0) parts.add('${e.bitrate} kbps'); + return parts.join(' · '); + } +} + +// ─── Artwork ──────────────────────────────────────────────────────────────── + +class _Artwork extends StatelessWidget { + final Emisora emisora; + final Stream estadoStream; + + const _Artwork({required this.emisora, required this.estadoStream}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final size = MediaQuery.of(context).size.width * 0.65; + + return StreamBuilder( + stream: estadoStream, + builder: (context, snapshot) { + final reproduciendo = snapshot.data == EstadoReproduccion.reproduciendo; + final cargando = snapshot.data == EstadoReproduccion.cargando; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: size, + height: size, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: reproduciendo + ? [ + BoxShadow( + color: theme.colorScheme.primary.withValues(alpha: 0.4), + blurRadius: 30, + spreadRadius: 5, + ), + ] + : [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 12, + spreadRadius: 2, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Stack( + fit: StackFit.expand, + children: [ + // Logo / imagen + if (emisora.favicon != null && emisora.favicon!.isNotEmpty) + CachedNetworkImage( + imageUrl: emisora.favicon!, + fit: BoxFit.cover, + placeholder: (_, __) => _shimmer(theme), + errorWidget: (_, __, ___) => _iconoFallback(theme), + ) + else + _iconoFallback(theme), + // Overlay de carga + if (cargando) + Container( + color: Colors.black45, + child: const Center( + child: CircularProgressIndicator(color: Colors.white), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _shimmer(ThemeData theme) => Shimmer.fromColors( + baseColor: theme.colorScheme.surfaceContainerHighest, + highlightColor: theme.colorScheme.surface, + child: Container(color: theme.colorScheme.surfaceContainerHighest), + ); + + Widget _iconoFallback(ThemeData theme) => Container( + color: theme.colorScheme.primaryContainer, + child: Icon( + Icons.radio_rounded, + size: 80, + color: theme.colorScheme.onPrimaryContainer, + ), + ); +} + +// ─── Info chips ───────────────────────────────────────────────────────────── + +class _InfoChips extends StatelessWidget { + final Emisora emisora; + const _InfoChips({required this.emisora}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final items = []; + if (emisora.pais != null) items.add(emisora.pais!); + if (emisora.idioma != null) items.add(emisora.idioma!); + if (items.isEmpty) return const SizedBox.shrink(); + + return Wrap( + spacing: 6, + children: items + .map((label) => Chip( + label: Text(label), + visualDensity: VisualDensity.compact, + backgroundColor: theme.colorScheme.secondaryContainer, + labelStyle: TextStyle( + color: theme.colorScheme.onSecondaryContainer, + fontSize: 12), + padding: EdgeInsets.zero, + )) + .toList(), + ); + } +} + +// ─── Controles ────────────────────────────────────────────────────────────── + +class _Controles extends StatelessWidget { + final EstadoRadio estado; + final Emisora emisora; + + const _Controles({required this.estado, required this.emisora}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return StreamBuilder( + stream: estado.estadoStream, + builder: (context, snapshot) { + final s = snapshot.data ?? EstadoReproduccion.detenido; + final reproduciendo = s == EstadoReproduccion.reproduciendo; + final cargando = s == EstadoReproduccion.cargando; + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Botón detener + IconButton( + icon: const Icon(Icons.stop_rounded), + iconSize: 36, + color: theme.colorScheme.onSurfaceVariant, + tooltip: 'Detener', + onPressed: cargando ? null : () => estado.audio.detener(), + ), + const SizedBox(width: 16), + // Botón play/pause principal + AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 72, + height: 72, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: theme.colorScheme.primary, + boxShadow: [ + BoxShadow( + color: theme.colorScheme.primary.withValues(alpha: 0.35), + blurRadius: reproduciendo ? 16 : 6, + spreadRadius: reproduciendo ? 4 : 0, + ), + ], + ), + child: Material( + color: Colors.transparent, + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: cargando + ? null + : () { + if (reproduciendo || s == EstadoReproduccion.pausado) { + estado.togglePlay(); + } else { + estado.reproducir(emisora); + } + }, + child: Center( + child: cargando + ? const SizedBox( + width: 28, + height: 28, + child: CircularProgressIndicator( + strokeWidth: 2.5, + color: Colors.white, + ), + ) + : Icon( + reproduciendo + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + size: 40, + color: theme.colorScheme.onPrimary, + ), + ), + ), + ), + ), + const SizedBox(width: 16), + // Indicador en vivo + Icon( + Icons.fiber_manual_record_rounded, + size: 36, + color: reproduciendo + ? theme.colorScheme.error + : theme.colorScheme.surfaceContainerHighest, + ), + ], + ); + }, + ); + } +} + +// ─── Timer widget ──────────────────────────────────────────────────────────── + +class _TimerWidget extends StatelessWidget { + final EstadoRadio estado; + const _TimerWidget({required this.estado}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + if (!estado.timer.activo) { + return TextButton.icon( + icon: const Icon(Icons.bedtime_outlined, size: 18), + label: const Text('Timer de sueño'), + onPressed: () => _mostrarTimerDialog(context), + ); + } + + return StreamBuilder( + stream: estado.timer.tiempoRestanteStream, + builder: (context, snap) { + final t = snap.data ?? Duration.zero; + final m = t.inMinutes.remainder(60).toString().padLeft(2, '0'); + final s = t.inSeconds.remainder(60).toString().padLeft(2, '0'); + final label = t.inHours > 0 + ? '${t.inHours}h ${m}m' + : '${m}m ${s}s'; + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.bedtime_rounded, size: 16, color: theme.colorScheme.primary), + const SizedBox(width: 6), + Text(label, style: theme.textTheme.bodyMedium), + const SizedBox(width: 8), + TextButton( + onPressed: () => estado.cancelarTimer(), + style: TextButton.styleFrom( + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + ), + child: const Text('Cancelar'), + ), + ], + ); + }, + ); + } + + void _mostrarTimerDialog(BuildContext context) { + 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), + Wrap( + spacing: 8, + children: opcionesTimer + .map((min) => ActionChip( + label: Text('$min min'), + onPressed: () { + estado.iniciarTimer(min); + Navigator.pop(ctx); + }, + )) + .toList(), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/mini_reproductor.dart b/lib/widgets/mini_reproductor.dart index 149d791..6690379 100644 --- a/lib/widgets/mini_reproductor.dart +++ b/lib/widgets/mini_reproductor.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../estado/estado_radio.dart'; +import '../pantallas/pantalla_reproductor.dart'; import '../servicios/servicio_audio.dart'; /// Barra inferior persistente con controles básicos de reproducción. -/// Se muestra siempre que haya una emisora cargada. +/// Toca la barra para abrir PantallaReproductor completa. class MiniReproductor extends StatelessWidget { const MiniReproductor({super.key}); @@ -17,80 +18,94 @@ class MiniReproductor extends StatelessWidget { final theme = Theme.of(context); - return Container( - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainer, - border: Border(top: BorderSide(color: theme.colorScheme.outlineVariant, width: 0.5)), - ), - child: SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - // Logo emisora - ClipRRect( - borderRadius: BorderRadius.circular(6), - child: Container( - width: 40, - height: 40, - color: theme.colorScheme.primaryContainer, - child: const Icon(Icons.radio, size: 22), + return GestureDetector( + onTap: () => PantallaReproductor.abrir(context, emisora), + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + border: Border( + top: BorderSide( + color: theme.colorScheme.outlineVariant, width: 0.5)), + ), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + // Logo + ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Container( + width: 40, + height: 40, + color: theme.colorScheme.primaryContainer, + child: Icon(Icons.radio, + size: 22, + color: theme.colorScheme.onPrimaryContainer), + ), ), - ), - const SizedBox(width: 12), - // Nombre y estado - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - emisora.nombre, - style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - StreamBuilder( - stream: estado.estadoStream, - builder: (context, snapshot) { - final s = snapshot.data ?? EstadoReproduccion.detenido; - return Text( - _labelEstado(s), - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ); - }, - ), - ], - ), - ), - // Botón play/pause - StreamBuilder( - stream: estado.estadoStream, - builder: (context, snapshot) { - final s = snapshot.data ?? EstadoReproduccion.detenido; - if (s == EstadoReproduccion.cargando) { - return const SizedBox( - width: 40, - height: 40, - child: Padding( - padding: EdgeInsets.all(10), - child: CircularProgressIndicator(strokeWidth: 2), + const SizedBox(width: 12), + // Nombre y estado + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + emisora.nombre, + style: theme.textTheme.bodyMedium + ?.copyWith(fontWeight: FontWeight.w600), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), + StreamBuilder( + stream: estado.estadoStream, + builder: (context, snapshot) { + final s = snapshot.data ?? + EstadoReproduccion.detenido; + return Text( + _labelEstado(s), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ); + }, + ), + ], + ), + ), + // Botón play/pause + StreamBuilder( + stream: estado.estadoStream, + builder: (context, snapshot) { + final s = + snapshot.data ?? EstadoReproduccion.detenido; + if (s == EstadoReproduccion.cargando) { + return const SizedBox( + width: 40, + height: 40, + child: Padding( + padding: EdgeInsets.all(10), + child: CircularProgressIndicator(strokeWidth: 2), + ), + ); + } + return IconButton( + icon: Icon( + s == EstadoReproduccion.reproduciendo + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + ), + onPressed: () { + // Evitar que el tap en el botón abra el reproductor + estado.togglePlay(); + }, ); - } - return IconButton( - icon: Icon(s == EstadoReproduccion.reproduciendo - ? Icons.pause_rounded - : Icons.play_arrow_rounded), - onPressed: estado.togglePlay, - tooltip: s == EstadoReproduccion.reproduciendo ? 'Pausar' : 'Reproducir', - ); - }, - ), - ], + }, + ), + ], + ), ), ), ), @@ -98,17 +113,12 @@ class MiniReproductor extends StatelessWidget { } String _labelEstado(EstadoReproduccion estado) { - switch (estado) { - case EstadoReproduccion.cargando: - return 'Conectando...'; - case EstadoReproduccion.reproduciendo: - return 'En directo ●'; - case EstadoReproduccion.pausado: - return 'Pausado'; - case EstadoReproduccion.error: - return 'Error de conexión'; - case EstadoReproduccion.detenido: - return 'Detenido'; - } + return switch (estado) { + EstadoReproduccion.cargando => 'Conectando...', + EstadoReproduccion.reproduciendo => 'En directo ●', + EstadoReproduccion.pausado => 'Pausado', + EstadoReproduccion.error => 'Error de conexión', + EstadoReproduccion.detenido => 'Detenido', + }; } } -- 2.49.1