refactor(state): extract export/import service and equalizer state from EstadoRadio

- New ServicioExportImport owns the v2 backup envelope, pretty JSON encode and graceful decode; byte-compatible with existing exports, locked by a round-trip test
- pantalla_ajustes delegates backup serialization to the service (inline jsonDecode/jsonEncode removed)
- New EstadoEcualizador ChangeNotifier owns all EQ state and persistence (principal/current/per-station presets, active flag), exposed via its own provider so EQ changes no longer rebuild EstadoRadio consumers
- EstadoRadio slims down ~210 lines and keeps 15 delegating compat members marked TODO(S4b) for the next slice to remove
- Player EQ toggle rewired to the new provider to avoid going stale
- 4 new tests (103 total green), flutter analyze clean
This commit is contained in:
2026-06-11 21:16:30 +02:00
parent 0380bbb1e7
commit 0416b301b2
10 changed files with 637 additions and 231 deletions
+207
View File
@@ -0,0 +1,207 @@
import 'package:flutter/foundation.dart';
import '../modelos/preset_ecualizador.dart';
import '../servicios/servicio_audio.dart';
import '../servicios/servicio_ecualizador.dart';
/// Equalizer state extracted from `EstadoRadio` (S4-R1).
///
/// Owns the main preset, the per-station preset map, the current (applied)
/// preset and the enabled flag, plus their persistence through
/// [ServicioEcualizador] and their application through [ServicioAudio].
/// Notifies ONLY its own listeners — EQ changes must not rebuild
/// `EstadoRadio` consumers (S4-R1-A, S4-R5).
class EstadoEcualizador extends ChangeNotifier {
EstadoEcualizador({
required this.audio,
ServicioEcualizador? servicio,
String? Function()? emisoraActualUuid,
}) : servicio = servicio ?? ServicioEcualizador(),
_emisoraActualUuid = emisoraActualUuid ?? (() => null);
final ServicioAudio audio;
final ServicioEcualizador servicio;
/// Callback into the owner (EstadoRadio) for the currently playing station;
/// keeps this notifier free of any station-list coupling.
final String? Function() _emisoraActualUuid;
final Map<String, PresetEcualizador> _presetsEmisoraMap = {};
PresetEcualizador _presetPrincipal = PresetEcualizador.flat;
PresetEcualizador _presetActual = PresetEcualizador.flat;
bool _activo = true;
PresetEcualizador get presetActual => _presetActual;
PresetEcualizador get presetPrincipal => _presetPrincipal;
bool get activo => _activo;
bool get disponible => audio.ecualizadorDisponible;
Map<String, PresetEcualizador> get presetsPorEmisora =>
Map.unmodifiable(_presetsEmisoraMap);
bool get emisoraActualTienePresetPropio {
final uuid = _emisoraActualUuid();
if (uuid == null) return false;
return tienePresetPorEmisora(uuid);
}
bool tienePresetPorEmisora(String uuid) =>
_presetsEmisoraMap.containsKey(uuid);
PresetEcualizador? presetPorEmisora(String uuid) => _presetsEmisoraMap[uuid];
PresetEcualizador presetParaEmisora(String uuid) =>
_presetsEmisoraMap[uuid] ?? _presetPrincipal;
/// Loads the persisted EQ configuration and applies it to the audio engine.
Future<void> cargarPersistido() async {
try {
final config = await servicio.cargar();
_presetPrincipal = config.principal;
_presetActual = config.principal;
_activo = config.activo;
_presetsEmisoraMap
..clear()
..addAll(config.porEmisora);
await audio.setEcualizadorActivo(_activo);
await audio.aplicarPreset(_presetPrincipal);
} catch (_) {
_presetPrincipal = PresetEcualizador.flat;
_presetActual = PresetEcualizador.flat;
_activo = true;
_presetsEmisoraMap.clear();
}
}
/// Applies [preset] to the audio engine and tracks it as current
/// WITHOUT persisting it (used when switching stations).
Future<void> aplicarPresetActivo(PresetEcualizador preset) async {
_presetActual = preset;
await audio.aplicarPreset(preset);
}
Future<void> cambiarPresetPrincipal(
PresetEcualizador preset, {
bool notificar = true,
}) async {
_presetPrincipal = preset;
await servicio.guardarPrincipal(preset);
final uuid = _emisoraActualUuid();
final puedeAplicarAhora =
uuid == null || !_presetsEmisoraMap.containsKey(uuid);
if (puedeAplicarAhora) {
await aplicarPresetActivo(preset);
}
if (notificar) notifyListeners();
}
Future<void> guardarPresetPorEmisora(
String uuid,
PresetEcualizador preset, {
bool notificar = true,
}) async {
_presetsEmisoraMap[uuid] = preset;
await servicio.guardarPorEmisora(uuid, preset);
if (_emisoraActualUuid() == uuid) {
await aplicarPresetActivo(preset);
}
if (notificar) notifyListeners();
}
Future<void> habilitarPresetPorEmisora(
String uuid, {
PresetEcualizador? base,
bool notificar = true,
}) async {
final presetBase = base ?? _presetsEmisoraMap[uuid] ?? _presetPrincipal;
await guardarPresetPorEmisora(uuid, presetBase, notificar: notificar);
}
Future<void> deshabilitarPresetPorEmisora(
String uuid, {
bool notificar = true,
}) async {
_presetsEmisoraMap.remove(uuid);
await servicio.eliminarPorEmisora(uuid);
if (_emisoraActualUuid() == uuid) {
await aplicarPresetActivo(_presetPrincipal);
}
if (notificar) notifyListeners();
}
Future<void> cambiarModoEmisoraActual({required bool usarPropio}) async {
final uuid = _emisoraActualUuid();
if (uuid == null) return;
if (usarPropio) {
await habilitarPresetPorEmisora(uuid);
} else {
await deshabilitarPresetPorEmisora(uuid);
}
}
Future<void> cambiarActivo(bool activo) async {
_activo = activo;
await servicio.guardarActivo(activo);
await audio.setEcualizadorActivo(activo);
if (activo) {
await audio.aplicarPreset(_presetActual);
}
notifyListeners();
}
Future<void> cambiarPreset(
PresetEcualizador preset, {
bool guardarPorEmisora = true,
}) async {
final uuid = _emisoraActualUuid();
final usarPresetPropio =
guardarPorEmisora &&
uuid != null &&
_presetsEmisoraMap.containsKey(uuid);
if (usarPresetPropio) {
await guardarPresetPorEmisora(uuid, preset);
return;
}
await cambiarPresetPrincipal(preset);
}
Future<void> cambiarBanda(int index, double db) async {
final bandas = List<double>.from(_presetActual.bandas);
if (index < 0 || index >= bandas.length) return;
bandas[index] = db;
final modificado = PresetEcualizador(
nombre: 'Personalizado',
bandas: bandas,
);
await cambiarPreset(modificado);
}
/// Replaces the whole EQ configuration (backup import path): persists it,
/// re-applies the preset effective for the current station and notifies.
Future<void> importarConfiguracion({
required PresetEcualizador principal,
required Map<String, PresetEcualizador> porEmisora,
}) async {
_presetPrincipal = principal;
_presetsEmisoraMap
..clear()
..addAll(porEmisora);
await servicio.guardarConfiguracion(
ConfiguracionEcualizador(
principal: _presetPrincipal,
porEmisora: _presetsEmisoraMap,
activo: _activo,
),
);
final uuid = _emisoraActualUuid();
await aplicarPresetActivo(
uuid == null ? _presetPrincipal : presetParaEmisora(uuid),
);
notifyListeners();
}
}