feat(ecualizador): include equalizer on/off toggle in export/import

The backup envelope carried favorites, EQ presets, alarms and the
multi-device toggle but not EstadoEcualizador's own on/off flag, so
restoring a backup on another device silently kept that device's
existing toggle state instead of the source device's.

Bumps the backup schema to v4 (additive over v3): the flag is only
written when explicitly provided, so old exports stay at v2/v3.
Importing an old backup without the field leaves the current toggle
untouched rather than defaulting it. Applying the imported value
reuses EstadoEcualizador.cambiarActivo so it persists and pushes to
the live audio engine exactly like a manual toggle.
This commit is contained in:
2026-08-28 23:07:01 +02:00
parent a2bed18937
commit 4ca2813267
7 changed files with 428 additions and 15 deletions
+14
View File
@@ -695,12 +695,21 @@ class EstadoEcualizador extends ChangeNotifier {
/// Replaces the whole EQ configuration (backup import path): persists it,
/// re-applies the preset effective for the current station and notifies.
///
/// [activo] is the imported on/off toggle (S4-R4/eq-export-toggle). When
/// `null` — an old backup with no `ecualizadorActivo` field — the CURRENT
/// toggle is left untouched: an absent flag must never flip the user's live
/// setting to an arbitrary value. When non-null, applies it through
/// [cambiarActivo], the same path a manual toggle uses, so the import
/// persists it AND pushes it to the live audio engine instead of just
/// updating [_activo] in memory.
Future<void> importarConfiguracion({
required PresetEcualizador principal,
required Map<String, PresetEcualizador> porEmisora,
Map<String, PresetEcualizador>? presetsDispositivo,
Map<String, PresetEcualizador>? presetsMatriz,
bool? eqMultiDeviceEnabled,
bool? activo,
}) async {
_presetPrincipal = principal;
_presetsEmisoraMap
@@ -735,6 +744,11 @@ class EstadoEcualizador extends ChangeNotifier {
final presetEfectivoActual =
uuid == null ? _presetPrincipal : _resolverPresetActivo();
await aplicarPresetActivo(presetEfectivoActual);
if (activo != null) {
await cambiarActivo(activo);
}
notifyListeners();
}
+17 -5
View File
@@ -807,9 +807,10 @@ class EstadoRadio extends ChangeNotifier {
static const _keyAlarmasConfig = 'alarmas_musicales_v1';
/// Genera el JSON de toda la configuración (v3 — portabilidad completa
/// con presets por dispositivo y matriz multi-device).
/// La forma del sobre v3 vive en [ServicioExportImport] (S4-R4).
/// Genera el JSON de toda la configuración (v4 — portabilidad completa
/// con presets por dispositivo, matriz multi-device y el toggle
/// on/off del ecualizador).
/// La forma del sobre vive en [ServicioExportImport] (S4-R4).
Future<Map<String, dynamic>> exportarConfig() async {
final favs = await favoritos.obtenerTodos();
final grupos = await favoritos.obtenerGrupos();
@@ -837,6 +838,8 @@ class EstadoRadio extends ChangeNotifier {
presetsPorDispositivo: ecualizador.presetsDispositivo,
presetsMatriz: ecualizador.presetsMatriz,
eqMultiDeviceEnabled: ecualizador.eqMultiDeviceEnabled,
// v4 extension — equalizer global on/off toggle.
ecualizadorActivo: ecualizador.activo,
);
}
@@ -850,10 +853,11 @@ class EstadoRadio extends ChangeNotifier {
/// Importa configuración desde un JSON exportado previamente.
/// Soporta v1 (sin grupos, sin alarmas), v2 (portabilidad completa),
/// y v3 (+ presets por dispositivo, presets matriz, toggle multi-device).
/// v3 (+ presets por dispositivo, presets matriz, toggle multi-device)
/// y v4 (+ toggle on/off del ecualizador).
Future<void> importarConfig(Map<String, dynamic> data) async {
final version = data['version'] as int? ?? 1;
if (version > 3) throw Exception(_textos.unsupportedConfigVersion);
if (version > 4) throw Exception(_textos.unsupportedConfigVersion);
final prefs = await _resolverPrefs();
@@ -932,12 +936,20 @@ class EstadoRadio extends ChangeNotifier {
eqMultiDeviceEnabled = data['eqMultiDeviceEnabled'] as bool?;
}
// v4 extension: equalizer on/off toggle. Read unconditionally — the key
// is simply absent on any pre-v4 backup, which resolves to `null` and
// leaves the user's CURRENT toggle untouched (see
// `EstadoEcualizador.importarConfiguracion` doc): an old backup must
// never flip a live setting it never carried.
final ecualizadorActivo = data['ecualizadorActivo'] as bool?;
await ecualizador.importarConfiguracion(
principal: presetPrincipal,
porEmisora: presetsPorEmisora,
presetsDispositivo: presetsDispositivo,
presetsMatriz: presetsMatriz,
eqMultiDeviceEnabled: eqMultiDeviceEnabled,
activo: ecualizadorActivo,
);
// ── Alarmas (v2) ──────────────────────────────────────────────────────
+35 -10
View File
@@ -7,26 +7,33 @@ import '../modelos/preset_ecualizador.dart';
/// Owns the backup (export/import) JSON serialization (S4-R4).
///
/// v3 extends v2 with `presetsPorDispositivo`, `presetsMatriz`, and
/// `eqMultiDeviceEnabled`. When those optional parameters are omitted the
/// export stays at v2 for backward compat with the old app. State APPLICATION
/// (writing favorites, EQ, alarms back into the app) stays in
/// `eqMultiDeviceEnabled`. v4 extends v3 with `ecualizadorActivo` (the
/// equalizer's global ON/OFF toggle). When the version-N extension
/// parameters are all omitted the export stays at the lower version for
/// backward compat with older app builds. State APPLICATION (writing
/// favorites, EQ, alarms back into the app) stays in
/// `EstadoRadio.importarConfig` — this service only owns serialization,
/// parsing and the envelope shape.
class ServicioExportImport {
const ServicioExportImport();
/// Current backup schema version (v3multi-device EQ).
static const int versionActual = 3;
/// Current backup schema version (v4equalizer on/off toggle).
static const int versionActual = 4;
/// v3 version constant (multi-device EQ) kept for clarity.
static const int versionV3 = 3;
/// Legacy v2 version constant kept for clarity.
static const int versionV2 = 2;
/// Builds the export envelope.
///
/// When [presetsPorDispositivo] or [presetsMatriz] are provided (non-null),
/// [versionActual] (3) is written. When both are omitted the call behaves
/// identically to the original v2 format (version key stays 2) so old
/// backups keep round-tripping without version bumps.
/// When [presetsPorDispositivo] or [presetsMatriz] or
/// [eqMultiDeviceEnabled] are provided (non-null), at least [versionV3] (3)
/// is written. When [ecualizadorActivo] is ALSO provided (non-null),
/// [versionActual] (4) is written. Omitting all of them behaves identically
/// to the original v2 format (version key stays 2) so old backups keep
/// round-tripping without version bumps.
///
/// The `alarmas` block is the RAW JSON map persisted by ServicioAlarmas
/// and passes through untouched (no re-parsing here).
@@ -45,14 +52,27 @@ class ServicioExportImport {
Map<String, PresetEcualizador>? presetsPorDispositivo,
Map<String, PresetEcualizador>? presetsMatriz,
bool? eqMultiDeviceEnabled,
// v4 extension — the equalizer's global ON/OFF toggle. Omitting it
// produces a v3 (or v2)-compatible export.
bool? ecualizadorActivo,
}) {
final tieneExtensionesV3 =
presetsPorDispositivo != null ||
presetsMatriz != null ||
eqMultiDeviceEnabled != null;
final tieneExtensionV4 = ecualizadorActivo != null;
final int version;
if (tieneExtensionV4) {
version = versionActual;
} else if (tieneExtensionesV3) {
version = versionV3;
} else {
version = versionV2;
}
final envelope = <String, dynamic>{
'version': tieneExtensionesV3 ? versionActual : versionV2,
'version': version,
'exportedAt': (exportadoEn ?? DateTime.now()).toIso8601String(),
// Favorites + groups (preserves grupo_id assignments per station).
// The protected "sin asignar" group is implicit and never exported.
@@ -88,6 +108,11 @@ class ServicioExportImport {
envelope['eqMultiDeviceEnabled'] = eqMultiDeviceEnabled ?? false;
}
// v4 extension: only written when explicitly provided.
if (tieneExtensionV4) {
envelope['ecualizadorActivo'] = ecualizadorActivo;
}
return envelope;
}