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
This commit is contained in:
@@ -6,21 +6,29 @@ import '../modelos/preset_ecualizador.dart';
|
||||
|
||||
/// Owns the backup (export/import) JSON serialization (S4-R4).
|
||||
///
|
||||
/// The v2 envelope produced here is byte-compatible with the legacy format
|
||||
/// previously assembled inline by `EstadoRadio.exportarConfig` and
|
||||
/// pretty-printed by `pantalla_ajustes.dart`, so existing exports keep
|
||||
/// round-tripping. State APPLICATION (writing favorites, EQ, alarms back
|
||||
/// into the app) stays in `EstadoRadio.importarConfig` — this service only
|
||||
/// owns serialization, parsing and the envelope shape.
|
||||
/// 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 (v2 — full portability).
|
||||
static const int versionActual = 2;
|
||||
/// Current backup schema version (v3 — multi-device EQ).
|
||||
static const int versionActual = 3;
|
||||
|
||||
/// Builds the v2 export envelope. Key set and semantics must stay exactly
|
||||
/// as the legacy inline export so old backups remain importable:
|
||||
/// the `alarmas` block is the RAW JSON map persisted by ServicioAlarmas
|
||||
/// 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,
|
||||
@@ -33,9 +41,18 @@ class ServicioExportImport {
|
||||
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,
|
||||
}) {
|
||||
return {
|
||||
'version': versionActual,
|
||||
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.
|
||||
@@ -47,7 +64,7 @@ class ServicioExportImport {
|
||||
'favoritos': favoritos.map((e) => e.toMap()).toList(),
|
||||
// Custom stations.
|
||||
'emisorasCustom': emisorasCustom.map((e) => e.toMap()).toList(),
|
||||
// Equalizer.
|
||||
// Equalizer (base fields — present in all versions).
|
||||
'presetPrincipalEcualizador': presetPrincipal.toJson(),
|
||||
'presetsEcualizador': presetsPorEmisora.map(
|
||||
(uuid, preset) => MapEntry(uuid, preset.toJson()),
|
||||
@@ -59,6 +76,19 @@ class ServicioExportImport {
|
||||
'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
|
||||
|
||||
Reference in New Issue
Block a user