fix(v0.3.0): audio background + emisoras rotas + errores toast + icono

- ServicioAudio: delega a PluriWaveAudioHandler (audio_service) para
  mantener audio vivo en background. AudioService.init() en main.dart.
  onTaskRemoved() libera player. mediaItem con nombre/artista/artwork.
- ServicioRadio: lastcheckok=1 en todas las peticiones — solo emisoras
  verificadas como funcionales por Radio Browser API.
- EstadoRadio: errorStream (broadcast) para errores de reproducción y
  búsqueda. App.dart suscribe y muestra SnackBar flotante 3s.
  Los errores de carga de lista siguen como banner inline.
- Icono: generado con SDXL (morado, ondas radio blancas, Material You).
  5 densidades Android (48-192px), ic_launcher_round añadido.
This commit is contained in:
Kira (Agent)
2026-04-04 18:09:59 +02:00
parent e9d1f67aa4
commit 81db383a47
18 changed files with 212 additions and 118 deletions

View File

@@ -27,7 +27,7 @@ class PluriWaveApp extends StatelessWidget {
ThemeData _buildTheme(Brightness brightness) {
final colorScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4), // Morado Material You
seedColor: const Color(0xFF6750A4),
brightness: brightness,
);
return ThemeData(
@@ -41,6 +41,10 @@ class PluriWaveApp extends StatelessWidget {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: colorScheme.surfaceContainerLow,
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
}
}
@@ -79,6 +83,22 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
),
];
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Suscribir al stream de errores → SnackBar flotante
context.read<EstadoRadio>().errorStream.listen((msg) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(msg),
duration: const Duration(seconds: 3),
action: SnackBarAction(label: 'OK', onPressed: () {}),
),
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -125,12 +145,15 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
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('${t.inHours > 0 ? "${t.inHours}h " : ""}${m}m ${s}s',
style: Theme.of(ctx).textTheme.headlineMedium),
Text(
'${h > 0 ? "${h}h " : ""}${m}m ${s}s',
style: Theme.of(ctx).textTheme.headlineMedium,
),
const SizedBox(height: 8),
FilledButton.tonal(
onPressed: () {
@@ -146,13 +169,15 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
else
Wrap(
spacing: 8,
children: [15, 30, 60, 90].map((min) => ActionChip(
label: Text('$min min'),
onPressed: () {
estado.iniciarTimer(min);
Navigator.pop(ctx);
},
)).toList(),
children: [15, 30, 60, 90]
.map((min) => ActionChip(
label: Text('$min min'),
onPressed: () {
estado.iniciarTimer(min);
Navigator.pop(ctx);
},
))
.toList(),
),
],
),