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
111 lines
4.3 KiB
Dart
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;
|
|
}
|
|
}
|
|
}
|