Files
pluriwave/lib/servicios/servicio_export_import.dart
T
FreeTLab 4632d53eb8 feat(eq): add per-device equalizer with 4-level preset resolution
Introduce multi-device EQ support allowing each audio output device
(built-in speaker, wired, USB, individual Bluetooth by MAC) to have
its own equalizer preset, combined with existing per-station presets
for a full station×device matrix.

- Add DispositivoAudio model and ServicioDispositivoAudio interface
- Add Android platform channel (AudioDeviceCallback) for device detection
- Add iOS AudioDevicesPlugin (AVAudioSession route tracking)
- Extend ServicioEcualizador with device and matrix persistence keys
- Implement 4-level resolution: matrix > station > device > global
- Add advanced EQ settings section with feature toggle (off by default)
- Extend export/import to v3 with backward compatibility
- 184 tests passing, zero analyzer issues
2026-06-27 11:33:53 +02:00

111 lines
4.3 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`. 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
/// `EstadoRadio.importarConfig` — this service only owns serialization,
/// parsing and the envelope shape.
class ServicioExportImport {
const ServicioExportImport();
/// Current backup schema version (v3 — multi-device EQ).
static const int versionActual = 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.
///
/// 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,
}) {
final tieneExtensionesV3 =
presetsPorDispositivo != null ||
presetsMatriz != null ||
eqMultiDeviceEnabled != null;
final envelope = <String, dynamic>{
'version': tieneExtensionesV3 ? versionActual : versionV2,
'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;
}
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;
}
}
}