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

@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../modelos/emisora.dart';
import '../servicios/servicio_audio.dart';
@@ -7,13 +8,18 @@ import '../servicios/servicio_timer.dart';
/// Estado global de la app con ChangeNotifier (Provider).
///
/// Centraliza: reproductoor, favoritos, búsqueda, timer.
/// Errores de reproducción se emiten por [errorStream] para mostrar como
/// SnackBar — no bloquean la UI.
class EstadoRadio extends ChangeNotifier {
final ServicioAudio audio = ServicioAudio();
final ServicioFavoritos favoritos = ServicioFavoritos();
final ServicioRadio radio = ServicioRadio();
late final ServicioTimer timer;
// Errores de reproducción → SnackBar en el UI
final _errorController = StreamController<String>.broadcast();
Stream<String> get errorStream => _errorController.stream;
List<Emisora> _populares = [];
List<Emisora> _tendencias = [];
List<Emisora> _resultadosBusqueda = [];
@@ -21,7 +27,7 @@ class EstadoRadio extends ChangeNotifier {
bool _cargandoPopulares = false;
bool _cargandoBusqueda = false;
String? _error;
String? _errorCarga; // solo para errores de carga de lista (banner estático)
EstadoRadio() {
timer = ServicioTimer(audio);
@@ -34,7 +40,7 @@ class EstadoRadio extends ChangeNotifier {
List<Emisora> get listaFavoritos => _listafavoritos;
bool get cargandoPopulares => _cargandoPopulares;
bool get cargandoBusqueda => _cargandoBusqueda;
String? get error => _error;
String? get error => _errorCarga;
Emisora? get emisoraActual => audio.emisoraActual;
Stream<EstadoReproduccion> get estadoStream => audio.estadoStream;
@@ -47,7 +53,7 @@ class EstadoRadio extends ChangeNotifier {
Future<void> cargarPopulares() async {
_cargandoPopulares = true;
_error = null;
_errorCarga = null;
notifyListeners();
try {
final results = await Future.wait([
@@ -57,7 +63,7 @@ class EstadoRadio extends ChangeNotifier {
_populares = results[0];
_tendencias = results[1];
} catch (e) {
_error = 'Error al cargar emisoras: $e';
_errorCarga = 'Sin conexión a la API de radio';
} finally {
_cargandoPopulares = false;
notifyListeners();
@@ -86,7 +92,8 @@ class EstadoRadio extends ChangeNotifier {
tag: tag,
);
} catch (e) {
_error = 'Error en búsqueda: $e';
// Error de búsqueda → toast, no bloquear pantalla
_errorController.add('Error en la búsqueda. Comprueba tu conexión.');
} finally {
_cargandoBusqueda = false;
notifyListeners();
@@ -99,8 +106,8 @@ class EstadoRadio extends ChangeNotifier {
radio.registrarClick(emisora.uuid); // fire & forget
notifyListeners();
} catch (e) {
_error = 'No se puede reproducir esta emisora';
notifyListeners();
// Error de reproducción → SnackBar, no pintar en medio de la UI
_errorController.add('No se puede reproducir "${emisora.nombre}"');
}
}
@@ -129,6 +136,7 @@ class EstadoRadio extends ChangeNotifier {
@override
void dispose() {
_errorController.close();
audio.dispose();
timer.dispose();
super.dispose();