Files
pluriwave/lib/servicios/servicio_export_import.dart
T
FreeTLab 4ca2813267 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.
2026-08-28 23:07:01 +02:00

136 lines
5.2 KiB
Dart

import 'dart:convert';
import '../modelos/emisora.dart';
import '../modelos/grupo_favoritos.dart';
import '../modelos/preset_ecualizador.dart';
/// Owns the backup (export/import) JSON serialization (S4-R4).
///
/// v3 extends v2 with `presetsPorDispositivo`, `presetsMatriz`, and
/// `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 (v4 — equalizer 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] 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).
Map<String, dynamic> construirExportacion({
required List<GrupoFavoritos> gruposFavoritos,
required List<Emisora> favoritos,
required List<Emisora> emisorasCustom,
required PresetEcualizador presetPrincipal,
required Map<String, PresetEcualizador> presetsPorEmisora,
required Map<String, dynamic>? alarmas,
required String? emisoraPreferidaUuid,
required String ordenListas,
required List<int> timerSuenoPresetsSegundos,
DateTime? exportadoEn,
// v3 extensions — omitting these produces a v2-compatible export.
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': version,
'exportedAt': (exportadoEn ?? DateTime.now()).toIso8601String(),
// Favorites + groups (preserves grupo_id assignments per station).
// The protected "sin asignar" group is implicit and never exported.
'gruposFavoritos':
gruposFavoritos
.where((g) => !g.esSinAsignar)
.map((g) => g.toMap())
.toList(),
'favoritos': favoritos.map((e) => e.toMap()).toList(),
// Custom stations.
'emisorasCustom': emisorasCustom.map((e) => e.toMap()).toList(),
// Equalizer (base fields — present in all versions).
'presetPrincipalEcualizador': presetPrincipal.toJson(),
'presetsEcualizador': presetsPorEmisora.map(
(uuid, preset) => MapEntry(uuid, preset.toJson()),
),
// Full alarm block (alarms + vacations + exceptions) — raw passthrough.
'alarmas': alarmas,
// User preferences.
'emisoraPreferidaUuid': emisoraPreferidaUuid,
'ordenListas': ordenListas,
'timerSuenoPresetsSegundos': timerSuenoPresetsSegundos,
};
// v3 extensions: only written when explicitly provided.
if (tieneExtensionesV3) {
envelope['presetsPorDispositivo'] = (presetsPorDispositivo ?? {}).map(
(deviceId, preset) => MapEntry(deviceId, preset.toJson()),
);
envelope['presetsMatriz'] = (presetsMatriz ?? {}).map(
(clave, preset) => MapEntry(clave, preset.toJson()),
);
envelope['eqMultiDeviceEnabled'] = eqMultiDeviceEnabled ?? false;
}
// v4 extension: only written when explicitly provided.
if (tieneExtensionV4) {
envelope['ecualizadorActivo'] = ecualizadorActivo;
}
return envelope;
}
/// Serializes an export envelope to pretty-printed JSON (same formatting
/// the legacy export shared as a file).
String exportar(Map<String, dynamic> config) =>
const JsonEncoder.withIndent(' ').convert(config);
/// Parses a backup JSON string. Returns `null` on malformed input or when
/// the document is not a JSON object — graceful, never throws (S4-R4).
Map<String, dynamic>? importar(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! Map) return null;
return Map<String, dynamic>.from(decoded);
} on FormatException {
return null;
}
}
}