feat(player): add radio recording and real waveform
Build & Deploy Pluriwave / Análisis de código (push) Successful in 12s
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 1m27s

This commit is contained in:
2026-05-21 21:17:51 +02:00
parent 6aa9a59d7b
commit a6a91af402
12 changed files with 1518 additions and 286 deletions
+77 -16
View File
@@ -12,6 +12,7 @@ import '../modelos/preset_ecualizador.dart';
import '../servicios/servicio_audio.dart';
import '../servicios/servicio_ecualizador.dart';
import '../servicios/servicio_favoritos.dart';
import '../servicios/servicio_grabacion_radio.dart';
import '../servicios/servicio_radio.dart';
import '../servicios/servicio_timer.dart';
@@ -22,15 +23,18 @@ class EstadoRadio extends ChangeNotifier {
ServicioFavoritos? favoritos,
ServicioRadio? radio,
ServicioEcualizador? servicioEcualizador,
ServicioGrabacionRadio? servicioGrabacion,
Future<File> Function()? resolverArchivoCustom,
bool iniciarAutomaticamente = true,
}) : audio = audio ?? ServicioAudio(),
favoritos = favoritos ?? ServicioFavoritos(),
radio = radio ?? ServicioRadio(),
servicioEcualizador = servicioEcualizador ?? ServicioEcualizador(),
_resolverArchivoCustom = resolverArchivoCustom {
}) : audio = audio ?? ServicioAudio(),
favoritos = favoritos ?? ServicioFavoritos(),
radio = radio ?? ServicioRadio(),
servicioEcualizador = servicioEcualizador ?? ServicioEcualizador(),
grabacion = servicioGrabacion ?? ServicioGrabacionRadio(),
_resolverArchivoCustom = resolverArchivoCustom {
timer = ServicioTimer(this.audio);
_escucharErroresReproduccion();
_escucharGrabacion();
if (iniciarAutomaticamente) {
_initFuture = _init();
}
@@ -40,10 +44,12 @@ class EstadoRadio extends ChangeNotifier {
final ServicioFavoritos favoritos;
final ServicioRadio radio;
final ServicioEcualizador servicioEcualizador;
final ServicioGrabacionRadio grabacion;
final Future<File> Function()? _resolverArchivoCustom;
late final ServicioTimer timer;
StreamSubscription<EstadoReproduccion>? _suscripcionEstadoAudio;
StreamSubscription<EstadoGrabacionRadio>? _suscripcionGrabacion;
Future<void>? _initFuture;
int _revisionReproduccion = 0;
Emisora? _emisoraSeleccionada;
@@ -110,6 +116,10 @@ class EstadoRadio extends ChangeNotifier {
return tienePresetEcualizadorPorEmisora(actual.uuid);
}
EstadoGrabacionRadio get estadoGrabacion => grabacion.estado;
bool get grabacionActiva => grabacion.estado.activa;
String? get directorioGrabacion => grabacion.directorioConfigurado;
/// Lista principal (home): custom + populares, sin duplicados.
List<Emisora> get emisorasInicio {
final mapa = <String, Emisora>{};
@@ -128,6 +138,7 @@ class EstadoRadio extends ChangeNotifier {
}
Future<void> _init() async {
await grabacion.inicializar();
await _cargarEcualizadorPersistido();
await Future.wait([
cargarPopulares(),
@@ -146,6 +157,16 @@ class EstadoRadio extends ChangeNotifier {
});
}
void _escucharGrabacion() {
_suscripcionGrabacion = grabacion.estadoStream.listen((estado) {
if (estado.tipo == EstadoGrabacionRadioTipo.error &&
estado.error != null) {
_errorController.add('Error al grabar la radio: ${estado.error}');
}
notifyListeners();
});
}
Future<void> _cargarEcualizadorPersistido() async {
try {
final config = await servicioEcualizador.cargar();
@@ -296,7 +317,8 @@ class EstadoRadio extends ChangeNotifier {
_paisCercanoDetectado = pais;
_emisorasCercanas = await radio.buscar(pais: pais, limit: 30);
} catch (_) {
_errorCercanas = 'No pudimos detectar emisoras cercanas. Usa filtros por pais.';
_errorCercanas =
'No pudimos detectar emisoras cercanas. Usa filtros por pais.';
_emisorasCercanas = [];
} finally {
_cargandoCercanas = false;
@@ -331,6 +353,34 @@ class EstadoRadio extends ChangeNotifier {
}
}
Future<void> iniciarGrabacion({Duration? duracion}) async {
final actual = emisoraActual;
if (actual == null) {
_errorController.add('Primero selecciona una emisora para grabar.');
return;
}
try {
await grabacion.iniciar(actual, duracion: duracion);
} catch (e) {
_errorController.add('No se pudo iniciar la grabación: $e');
}
}
Future<void> detenerGrabacion() => grabacion.detener();
Future<void> cambiarDirectorioGrabacion(String path) async {
await grabacion.guardarDirectorio(path);
notifyListeners();
}
Future<void> restaurarDirectorioGrabacion() async {
await grabacion.limpiarDirectorioConfigurado();
notifyListeners();
}
Future<String> directorioGrabacionEfectivo() =>
grabacion.directorioEfectivo();
Future<void> togglePlay() async {
await audio.togglePlay();
notifyListeners();
@@ -339,7 +389,10 @@ class EstadoRadio extends ChangeNotifier {
Future<bool> toggleFavorito(Emisora emisora) async {
final esFav = await favoritos.toggleFavorito(emisora);
if (!esFav) {
await deshabilitarPresetEcualizadorPorEmisora(emisora.uuid, notificar: false);
await deshabilitarPresetEcualizadorPorEmisora(
emisora.uuid,
notificar: false,
);
}
await cargarFavoritos();
return esFav;
@@ -418,7 +471,9 @@ class EstadoRadio extends ChangeNotifier {
if (notificar) notifyListeners();
}
Future<void> cambiarModoEcualizadorEmisoraActual({required bool usarPropio}) async {
Future<void> cambiarModoEcualizadorEmisoraActual({
required bool usarPropio,
}) async {
final actual = emisoraActual;
if (actual == null) return;
if (usarPropio) {
@@ -433,7 +488,8 @@ class EstadoRadio extends ChangeNotifier {
bool guardarPorEmisora = true,
}) async {
final actual = emisoraActual;
final usarPresetPropio = guardarPorEmisora &&
final usarPresetPropio =
guardarPorEmisora &&
actual != null &&
_presetsEmisoraMap.containsKey(actual.uuid);
@@ -476,9 +532,10 @@ class EstadoRadio extends ChangeNotifier {
}
final data = jsonDecode(await archivo.readAsString()) as List;
_emisorasCustom = data
.map((e) => Emisora.fromMap(Map<String, dynamic>.from(e as Map)))
.toList();
_emisorasCustom =
data
.map((e) => Emisora.fromMap(Map<String, dynamic>.from(e as Map)))
.toList();
} catch (_) {
_emisorasCustom = [];
}
@@ -510,7 +567,8 @@ class EstadoRadio extends ChangeNotifier {
}
// Compatibilidad con el nombre histórico (typo original).
Future<void> eliminarEmitoraCustom(String uuid) => eliminarEmisoraCustom(uuid);
Future<void> eliminarEmitoraCustom(String uuid) =>
eliminarEmisoraCustom(uuid);
// ── Export / Import ───────────────────────────────────────────────────────
@@ -541,9 +599,10 @@ class EstadoRadio extends ChangeNotifier {
}
final customRaw = data['emisorasCustom'] as List? ?? [];
_emisorasCustom = customRaw
.map((e) => Emisora.fromMap(Map<String, dynamic>.from(e as Map)))
.toList();
_emisorasCustom =
customRaw
.map((e) => Emisora.fromMap(Map<String, dynamic>.from(e as Map)))
.toList();
await _guardarEmisorasCustom();
final principalRaw = data['presetPrincipalEcualizador'];
@@ -600,8 +659,10 @@ class EstadoRadio extends ChangeNotifier {
@override
void dispose() {
_suscripcionEstadoAudio?.cancel();
_suscripcionGrabacion?.cancel();
_errorController.close();
audio.dispose();
unawaited(grabacion.dispose());
timer.dispose();
super.dispose();
}