refactor(state): extract recording and search state, scope screen rebuilds

- New EstadoGrabacion owns the recording service, subscription, directory/size preferences and open-file actions
- New EstadoBusqueda owns search, nearby stations, pagination and the min-bitrate filter
- New orden_emisoras.dart with the OrdenEmisoras enum, shared sorter and list identity memoization so context.select comparisons work on derived lists
- Large screens (inicio, buscar, favoritos, ajustes, reproductor) consume scoped selects/dedicated notifiers instead of root context.watch<EstadoRadio>, so audio buffer events no longer rebuild whole screens
- Remove all 15 TODO(S4b) compat members from EstadoRadio; consumers use the dedicated providers. EstadoRadio drops from ~1121 to 753 lines, keeping playback/stations/favorites orchestration
- 8 new tests including a rebuild-scoping probe (110 total green), flutter analyze clean
This commit is contained in:
2026-06-11 21:43:18 +02:00
parent 0416b301b2
commit 52855e75c2
17 changed files with 1195 additions and 643 deletions
+116 -413
View File
@@ -3,20 +3,19 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart' show Locale;
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
import '../l10n/display_names.dart';
import '../l10n/gen/app_localizations.dart';
import '../modelos/emisora.dart';
import '../modelos/grupo_favoritos.dart';
import '../modelos/preset_ecualizador.dart';
import 'estado_busqueda.dart';
import 'estado_ecualizador.dart';
import 'estado_grabacion.dart';
import 'orden_emisoras.dart';
import '../servicios/servicio_audio.dart';
import '../servicios/servicio_ecualizador.dart';
import '../servicios/servicio_export_import.dart';
@@ -25,14 +24,15 @@ import '../servicios/servicio_grabacion_radio.dart';
import '../servicios/servicio_radio.dart';
import '../servicios/servicio_timer.dart';
enum OrdenEmisoras { nombre, calidad }
export 'orden_emisoras.dart' show OrdenEmisoras;
/// Estado global de la app con ChangeNotifier (Provider).
///
/// S4 end-state: playback + stations + favorites orchestration. EQ, recording
/// and search state live in their own notifiers (EstadoEcualizador,
/// EstadoGrabacion, EstadoBusqueda) created here during the S4 transition and
/// exposed app-wide through ListenableProviders in app.dart.
class EstadoRadio extends ChangeNotifier {
static const MethodChannel _fileActionsChannel = MethodChannel(
'pluriwave/file_actions',
);
EstadoRadio({
ServicioAudio? audio,
ServicioFavoritos? favoritos,
@@ -47,7 +47,6 @@ class EstadoRadio extends ChangeNotifier {
radio = radio ?? ServicioRadio(),
servicioEcualizador =
servicioEcualizador ?? ServicioEcualizador(prefs: prefs),
grabacion = servicioGrabacion ?? ServicioGrabacionRadio(prefs: prefs),
_prefs = prefs,
_resolverArchivoCustom = resolverArchivoCustom {
ecualizador = EstadoEcualizador(
@@ -55,9 +54,19 @@ class EstadoRadio extends ChangeNotifier {
servicio: this.servicioEcualizador,
emisoraActualUuid: () => emisoraActual?.uuid,
);
grabacion = EstadoGrabacion(
servicio: servicioGrabacion ?? ServicioGrabacionRadio(prefs: prefs),
emisoraActual: () => emisoraActual,
alError: _errorController.add,
);
busqueda = EstadoBusqueda(
radio: this.radio,
ordenListas: () => _ordenListas,
textos: () => _textos,
alError: _errorController.add,
);
timer = ServicioTimer(this.audio);
_escucharErroresReproduccion();
_escucharGrabacion();
if (iniciarAutomaticamente) {
_initFuture = _init();
}
@@ -68,11 +77,12 @@ class EstadoRadio extends ChangeNotifier {
final ServicioRadio radio;
final ServicioEcualizador servicioEcualizador;
/// EQ state extracted to its own notifier (S4-R1). Owned (and disposed)
/// by EstadoRadio during the S4 transition; exposed app-wide through a
/// ListenableProvider in app.dart.
/// Domain notifiers extracted from this class (S4). Created and disposed
/// here (they need EstadoRadio's services and callbacks at construction);
/// exposed app-wide through ListenableProviders in app.dart.
late final EstadoEcualizador ecualizador;
final ServicioGrabacionRadio grabacion;
late final EstadoGrabacion grabacion;
late final EstadoBusqueda busqueda;
static const ServicioExportImport _exportImport = ServicioExportImport();
final SharedPreferences? _prefs;
final Future<File> Function()? _resolverArchivoCustom;
@@ -99,7 +109,6 @@ class EstadoRadio extends ChangeNotifier {
late final ServicioTimer timer;
StreamSubscription<EstadoReproduccion>? _suscripcionEstadoAudio;
StreamSubscription<EstadoGrabacionRadio>? _suscripcionGrabacion;
Future<void>? _initFuture;
int _revisionReproduccion = 0;
Emisora? _emisoraSeleccionada;
@@ -112,26 +121,23 @@ class EstadoRadio extends ChangeNotifier {
List<Emisora> _populares = [];
List<Emisora> _tendencias = [];
List<Emisora> _resultadosBusqueda = [];
List<Emisora> _emisorasCercanas = [];
List<Emisora> _listaFavoritos = [];
List<GrupoFavoritos> _gruposFavoritos = [];
List<Emisora> _emisorasCustom = [];
bool _cargandoPopulares = false;
bool _cargandoBusqueda = false;
bool _cargandoMasBusqueda = false;
bool _hayMasBusqueda = true;
bool _cargandoCercanas = false;
String? _paisCercanoDetectado;
String? _errorCercanas;
int _offsetBusqueda = 0;
String? _ultimoNombreBusqueda;
String? _ultimoPaisBusqueda;
String? _ultimoIdiomaBusqueda;
String? _ultimoTagBusqueda;
int? _ultimoMinBitrateBusqueda;
String? _errorCarga;
// Identity-memoized derived lists so `context.select` consumers only
// rebuild when the underlying data actually changes (S4-R5).
final _memoPopulares = MemoLista<Emisora>();
final _memoTendencias = MemoLista<Emisora>();
final _memoFavoritos = MemoLista<Emisora>();
final _memoGrupos = MemoLista<GrupoFavoritos>();
final _memoCustom = MemoLista<Emisora>();
final _memoInicio = MemoLista<Emisora>();
final _memoDisponibles = MemoLista<Emisora>();
final _memoTimerPresets = MemoLista<int>();
static const _keyEmisoraPreferida = 'emisora_preferida_uuid_v1';
static const _keyOrdenListas = 'orden_listas_emisoras_v1';
static const _keyTimerSuenoPresets = 'timer_sueno_presets_segundos_v1';
@@ -151,38 +157,35 @@ class EstadoRadio extends ChangeNotifier {
);
OrdenEmisoras _ordenListas = OrdenEmisoras.calidad;
List<Emisora> get populares => _ordenarEmisoras(_populares);
List<Emisora> get tendencias => _ordenarEmisoras(_tendencias);
List<Emisora> get resultadosBusqueda => _ordenarEmisoras(_resultadosBusqueda);
List<Emisora> get emisorasCercanas => _ordenarEmisoras(_emisorasCercanas);
List<Emisora> get listaFavoritos => _ordenarEmisoras(_listaFavoritos);
List<GrupoFavoritos> get gruposFavoritos =>
List.unmodifiable(_gruposFavoritos);
List<Emisora> get emisorasCustom => _ordenarEmisoras(_emisorasCustom);
List<Emisora> get populares => _memoPopulares.obtener([
_populares,
_ordenListas,
], () => ordenarEmisoras(_populares, _ordenListas));
List<Emisora> get tendencias => _memoTendencias.obtener([
_tendencias,
_ordenListas,
], () => ordenarEmisoras(_tendencias, _ordenListas));
List<Emisora> get listaFavoritos => _memoFavoritos.obtener([
_listaFavoritos,
_ordenListas,
], () => ordenarEmisoras(_listaFavoritos, _ordenListas));
List<GrupoFavoritos> get gruposFavoritos => _memoGrupos.obtener([
_gruposFavoritos,
], () => List<GrupoFavoritos>.unmodifiable(_gruposFavoritos));
List<Emisora> get emisorasCustom => _memoCustom.obtener([
_emisorasCustom,
_ordenListas,
], () => ordenarEmisoras(_emisorasCustom, _ordenListas));
bool get cargandoPopulares => _cargandoPopulares;
bool get cargandoBusqueda => _cargandoBusqueda;
bool get cargandoMasBusqueda => _cargandoMasBusqueda;
bool get hayMasBusqueda => _hayMasBusqueda;
bool get cargandoCercanas => _cargandoCercanas;
String? get paisCercanoDetectado => _paisCercanoDetectado;
String? get errorCercanas => _errorCercanas;
String? get error => _errorCarga;
Emisora? get emisoraActual => _emisoraSeleccionada ?? audio.emisoraActual;
Emisora? get emisoraPreferida => _resolverEmisoraPreferida();
String? get emisoraPreferidaUuid => emisoraPreferida?.uuid;
Stream<EstadoReproduccion> get estadoStream => audio.estadoStream;
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
PresetEcualizador get presetEcualizador => ecualizador.presetActual;
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
PresetEcualizador get presetPrincipalEcualizador =>
ecualizador.presetPrincipal;
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
bool get ecualizadorActivo => ecualizador.activo;
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
bool get ecualizadorDisponible => ecualizador.disponible;
OrdenEmisoras get ordenListas => _ordenListas;
List<int> get timerSuenoPresetsSegundos =>
List<int>.unmodifiable(_timerSuenoPresetsSegundos);
List<int> get timerSuenoPresetsSegundos => _memoTimerPresets.obtener([
_timerSuenoPresetsSegundos,
], () => List<int>.unmodifiable(_timerSuenoPresetsSegundos));
bool get emisoraActualEsFavorita {
final actual = emisoraActual;
@@ -190,50 +193,51 @@ class EstadoRadio extends ChangeNotifier {
return _listaFavoritos.any((e) => e.uuid == actual.uuid);
}
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
bool get emisoraActualTienePresetPropio =>
ecualizador.emisoraActualTienePresetPropio;
EstadoGrabacionRadio get estadoGrabacion => grabacion.estado;
bool get grabacionActiva => grabacion.estado.activa;
String? get directorioGrabacion => grabacion.directorioConfigurado;
int get maxBytesGrabacion => grabacion.maxBytes;
File? get ultimaGrabacion => grabacion.ultimoArchivo;
/// Lista principal (home): custom + populares, sin duplicados.
List<Emisora> get emisorasInicio {
final mapa = <String, Emisora>{};
for (final emisora in _emisorasCustom) {
mapa[emisora.uuid] = emisora;
}
for (final emisora in _populares) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
return mapa.values.toList();
}
List<Emisora> get emisorasInicio =>
_memoInicio.obtener([_emisorasCustom, _populares], () {
final mapa = <String, Emisora>{};
for (final emisora in _emisorasCustom) {
mapa[emisora.uuid] = emisora;
}
for (final emisora in _populares) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
return mapa.values.toList();
});
List<Emisora> get emisorasDisponiblesPreferencia {
final mapa = <String, Emisora>{};
for (final emisora in _listaFavoritos) {
mapa[emisora.uuid] = emisora;
}
for (final emisora in _emisorasCustom) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
for (final emisora in _populares) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
for (final emisora in _tendencias) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
for (final emisora in _resultadosBusqueda) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
for (final emisora in _emisorasCercanas) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
return mapa.values.toList();
}
List<Emisora> get emisorasDisponiblesPreferencia => _memoDisponibles.obtener(
[
_listaFavoritos,
_emisorasCustom,
_populares,
_tendencias,
busqueda.resultados,
busqueda.cercanas,
],
() {
final mapa = <String, Emisora>{};
for (final emisora in _listaFavoritos) {
mapa[emisora.uuid] = emisora;
}
for (final emisora in _emisorasCustom) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
for (final emisora in _populares) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
for (final emisora in _tendencias) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
for (final emisora in busqueda.resultados) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
for (final emisora in busqueda.cercanas) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
return mapa.values.toList();
},
);
Future<void> inicializar() {
_initFuture ??= _init();
@@ -264,23 +268,13 @@ class EstadoRadio extends ChangeNotifier {
if ((estado == EstadoReproduccion.detenido ||
estado == EstadoReproduccion.pausado ||
estado == EstadoReproduccion.error) &&
grabacion.estado.activa) {
grabacion.activa) {
unawaited(grabacion.detener());
}
notifyListeners();
});
}
void _escucharGrabacion() {
_suscripcionGrabacion = grabacion.estadoStream.listen((estado) {
if (estado.tipo == EstadoGrabacionRadioTipo.error &&
estado.error != null) {
_errorController.add(_textos.radioRecordingError(estado.error!));
}
notifyListeners();
});
}
Future<void> cargarPopulares() async {
_cargandoPopulares = true;
_errorCarga = null;
@@ -390,6 +384,8 @@ class EstadoRadio extends ChangeNotifier {
_ordenListas = orden;
final prefs = await _resolverPrefs();
await prefs.setString(_keyOrdenListas, orden.name);
// Search owns its own listeners (S4-R3) but sorts with this preference.
busqueda.notificarCambioOrden();
notifyListeners();
}
@@ -422,176 +418,9 @@ class EstadoRadio extends ChangeNotifier {
return disponibles.isEmpty ? null : disponibles.first;
}
static const int _tamanoPaginaBusqueda = 30;
static const int _maxResultadosBusquedaEnMemoria = 180;
Future<void> buscar({
String? nombre,
String? pais,
String? idioma,
String? tag,
int? minBitrate,
}) async {
_ultimoNombreBusqueda = nombre;
_ultimoPaisBusqueda = pais;
_ultimoIdiomaBusqueda = idioma;
_ultimoTagBusqueda = tag;
_ultimoMinBitrateBusqueda = minBitrate;
_offsetBusqueda = 0;
_hayMasBusqueda = true;
_cargandoBusqueda = true;
_resultadosBusqueda = [];
notifyListeners();
try {
final pagina = await _buscarPaginaFiltrada(
nombre: nombre,
pais: pais,
idioma: idioma,
tag: tag,
minBitrate: minBitrate,
);
_resultadosBusqueda = pagina;
} catch (_) {
_errorController.add(_textos.radioSearchError);
} finally {
_cargandoBusqueda = false;
notifyListeners();
}
}
Future<void> cargarMasBusqueda() async {
if (_cargandoBusqueda || _cargandoMasBusqueda || !_hayMasBusqueda) return;
_cargandoMasBusqueda = true;
notifyListeners();
try {
final pagina = await _buscarPaginaFiltrada(
nombre: _ultimoNombreBusqueda,
pais: _ultimoPaisBusqueda,
idioma: _ultimoIdiomaBusqueda,
tag: _ultimoTagBusqueda,
minBitrate: _ultimoMinBitrateBusqueda,
);
final porUuid = <String, Emisora>{
for (final emisora in _resultadosBusqueda) emisora.uuid: emisora,
};
for (final emisora in pagina) {
porUuid[emisora.uuid] = emisora;
}
var nuevaLista = porUuid.values.toList();
if (nuevaLista.length > _maxResultadosBusquedaEnMemoria) {
nuevaLista = nuevaLista.sublist(
nuevaLista.length - _maxResultadosBusquedaEnMemoria,
);
}
_resultadosBusqueda = nuevaLista;
// _buscarPaginaFiltrada actualiza offset/hayMas usando páginas crudas.
_hayMasBusqueda = _hayMasBusqueda && pagina.isNotEmpty;
} catch (_) {
_errorController.add(_textos.radioLoadMoreStationsError);
} finally {
_cargandoMasBusqueda = false;
notifyListeners();
}
}
Future<List<Emisora>> _buscarPaginaFiltrada({
String? nombre,
String? pais,
String? idioma,
String? tag,
int? minBitrate,
}) async {
final acumuladas = <Emisora>[];
var intentos = 0;
while (intentos < 4 && acumuladas.isEmpty && _hayMasBusqueda) {
final pagina = await radio.buscar(
nombre: nombre,
pais: pais,
idioma: idioma,
tag: tag,
limit: _tamanoPaginaBusqueda,
offset: _offsetBusqueda,
);
_offsetBusqueda += pagina.length;
_hayMasBusqueda = pagina.length == _tamanoPaginaBusqueda;
acumuladas.addAll(_filtrarMinBitrate(pagina, minBitrate));
intentos++;
}
return acumuladas;
}
List<Emisora> _filtrarMinBitrate(List<Emisora> emisoras, int? minBitrate) {
if (minBitrate == null || minBitrate <= 0) return emisoras;
return emisoras.where((e) => (e.bitrate ?? 0) >= minBitrate).toList();
}
List<Emisora> _ordenarEmisoras(List<Emisora> emisoras) {
final ordenadas = List<Emisora>.from(emisoras);
switch (_ordenListas) {
case OrdenEmisoras.nombre:
ordenadas.sort(
(a, b) => a.nombre.toLowerCase().compareTo(b.nombre.toLowerCase()),
);
case OrdenEmisoras.calidad:
ordenadas.sort((a, b) {
final porBitrate = (b.bitrate ?? 0).compareTo(a.bitrate ?? 0);
if (porBitrate != 0) return porBitrate;
return 0;
});
}
return ordenadas;
}
Future<void> cargarEmisorasCercanas() async {
_cargandoCercanas = true;
_errorCercanas = null;
notifyListeners();
try {
var pais = PlatformDispatcher.instance.locale.countryCode;
final servicioActivo = await Geolocator.isLocationServiceEnabled();
if (servicioActivo) {
var permiso = await Geolocator.checkPermission();
if (permiso == LocationPermission.denied) {
permiso = await Geolocator.requestPermission();
}
if (permiso == LocationPermission.always ||
permiso == LocationPermission.whileInUse) {
final posicion = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.low,
timeLimit: Duration(seconds: 8),
),
);
final marcas = await placemarkFromCoordinates(
posicion.latitude,
posicion.longitude,
);
if (marcas.isNotEmpty) {
pais = marcas.first.isoCountryCode ?? pais;
}
}
}
if (pais == null || pais.isEmpty) {
throw StateError('nearby-region-not-detected');
}
_paisCercanoDetectado = pais;
_emisorasCercanas = _filtrarMinBitrate(
await radio.buscar(pais: pais, limit: 30),
_ultimoMinBitrateBusqueda,
);
} catch (_) {
_errorCercanas = _textos.radioNearbyStationsError;
_emisorasCercanas = [];
} finally {
_cargandoCercanas = false;
notifyListeners();
}
}
Future<void> reproducir(Emisora emisora) async {
final revision = ++_revisionReproduccion;
if (grabacion.estado.activa) {
if (grabacion.activa) {
await grabacion.detener();
}
_emisoraSeleccionada = emisora;
@@ -623,83 +452,16 @@ class EstadoRadio extends ChangeNotifier {
}
}
Future<void> iniciarGrabacion({Duration? duracion}) async {
final actual = emisoraActual;
if (actual == null) {
_errorController.add(_textos.recordingSelectStationFirst);
return;
}
try {
await grabacion.iniciar(actual, duracion: duracion);
} catch (e) {
_errorController.add(_textos.recordingStartError(e.toString()));
}
}
Future<void> detenerGrabacion() => grabacion.detener();
Future<void> detenerReproduccion() async {
if (grabacion.estado.activa) {
if (grabacion.activa) {
await grabacion.detener();
}
await audio.detener();
notifyListeners();
}
Future<void> cambiarMaxBytesGrabacion(int bytes) async {
await grabacion.guardarMaxBytes(bytes);
notifyListeners();
}
Future<bool> abrirDirectorioGrabacion() async {
final ruta = await directorioGrabacionEfectivo();
await Directory(ruta).create(recursive: true);
if (!kIsWeb && Platform.isAndroid) {
final abierto = await _fileActionsChannel.invokeMethod<bool>(
'viewDirectory',
{'path': ruta},
);
return abierto ?? false;
}
final uri = Uri.directory(ruta);
return launchUrl(uri, mode: LaunchMode.externalApplication);
}
Future<bool> abrirUltimaGrabacion() async {
final archivo = ultimaGrabacion;
if (archivo == null || !await archivo.exists()) {
debugPrint('[PluriWave][recordings] last recording missing');
return false;
}
debugPrint('[PluriWave][recordings] opening last file: ${archivo.path}');
if (!kIsWeb && Platform.isAndroid) {
final abierto = await _fileActionsChannel.invokeMethod<bool>('openFile', {
'path': archivo.path,
'mimeType': 'audio/*',
});
return abierto ?? false;
}
return launchUrl(
Uri.file(archivo.path),
mode: LaunchMode.externalApplication,
);
}
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 {
if (audio.estaSonando && grabacion.estado.activa) {
if (audio.estaSonando && grabacion.activa) {
await grabacion.detener();
}
await audio.togglePlay();
@@ -709,7 +471,7 @@ class EstadoRadio extends ChangeNotifier {
Future<bool> toggleFavorito(Emisora emisora) async {
final esFav = await favoritos.toggleFavorito(emisora);
if (!esFav) {
await deshabilitarPresetEcualizadorPorEmisora(
await ecualizador.deshabilitarPresetPorEmisora(
emisora.uuid,
notificar: false,
);
@@ -720,68 +482,6 @@ class EstadoRadio extends ChangeNotifier {
Future<bool> esFavorito(String uuid) => favoritos.esFavorito(uuid);
// ── Ecualizador ───────────────────────────────────────────────────────────
// Transition bridge (S4a): EQ state lives in EstadoEcualizador; these
// delegating members keep legacy call sites compiling. They do NOT notify
// EstadoRadio listeners (S4-R1-A).
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
bool tienePresetEcualizadorPorEmisora(String uuid) =>
ecualizador.tienePresetPorEmisora(uuid);
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
PresetEcualizador? presetEcualizadorPorEmisora(String uuid) =>
ecualizador.presetPorEmisora(uuid);
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
Future<void> cambiarPresetPrincipalEcualizador(
PresetEcualizador preset, {
bool notificar = true,
}) => ecualizador.cambiarPresetPrincipal(preset, notificar: notificar);
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
Future<void> guardarPresetEcualizadorPorEmisora(
String uuid,
PresetEcualizador preset, {
bool notificar = true,
}) => ecualizador.guardarPresetPorEmisora(uuid, preset, notificar: notificar);
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
Future<void> habilitarPresetEcualizadorPorEmisora(
String uuid, {
PresetEcualizador? base,
bool notificar = true,
}) => ecualizador.habilitarPresetPorEmisora(
uuid,
base: base,
notificar: notificar,
);
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
Future<void> deshabilitarPresetEcualizadorPorEmisora(
String uuid, {
bool notificar = true,
}) => ecualizador.deshabilitarPresetPorEmisora(uuid, notificar: notificar);
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
Future<void> cambiarModoEcualizadorEmisoraActual({
required bool usarPropio,
}) => ecualizador.cambiarModoEmisoraActual(usarPropio: usarPropio);
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
Future<void> cambiarEcualizadorActivo(bool activo) =>
ecualizador.cambiarActivo(activo);
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
Future<void> cambiarPresetEcualizador(
PresetEcualizador preset, {
bool guardarPorEmisora = true,
}) => ecualizador.cambiarPreset(preset, guardarPorEmisora: guardarPorEmisora);
// TODO(S4b): remove getter — consumers migrate to EstadoEcualizador.
Future<void> cambiarBandaEcualizador(int index, double db) =>
ecualizador.cambiarBanda(index, db);
// ── Emisoras personalizadas ───────────────────────────────────────────────
Future<File> _archivoCustom() async {
@@ -820,8 +520,11 @@ class EstadoRadio extends ChangeNotifier {
}
Future<void> agregarEmisoraCustom(Emisora emisora) async {
_emisorasCustom.removeWhere((e) => e.uuid == emisora.uuid);
_emisorasCustom.add(emisora);
// Reassign (not mutate) so identity-memoized views refresh (S4-R5).
_emisorasCustom = [
..._emisorasCustom.where((e) => e.uuid != emisora.uuid),
emisora,
];
await _guardarEmisorasCustom();
notifyListeners();
}
@@ -831,7 +534,7 @@ class EstadoRadio extends ChangeNotifier {
agregarEmisoraCustom(emisora);
Future<void> eliminarEmisoraCustom(String uuid) async {
_emisorasCustom.removeWhere((e) => e.uuid == uuid);
_emisorasCustom = _emisorasCustom.where((e) => e.uuid != uuid).toList();
await _guardarEmisorasCustom();
notifyListeners();
}
@@ -1039,11 +742,11 @@ class EstadoRadio extends ChangeNotifier {
@override
void dispose() {
_suscripcionEstadoAudio?.cancel();
_suscripcionGrabacion?.cancel();
_errorController.close();
ecualizador.dispose();
busqueda.dispose();
grabacion.dispose();
audio.dispose();
unawaited(grabacion.dispose());
timer.dispose();
super.dispose();
}