feat(v0.4.0): PantallaReproductor + PantallaAjustes + MiniReproductor tappable

- 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.
This commit is contained in:
Kira (Agent)
2026-04-04 18:24:09 +02:00
parent 81db383a47
commit ac5ab2316f
5 changed files with 677 additions and 95 deletions

View File

@@ -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<EstadoReproduccion>(
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<EstadoReproduccion>(
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<EstadoReproduccion>(
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<EstadoReproduccion>(
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',
};
}
}