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
221 lines
6.9 KiB
Dart
221 lines
6.9 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../modelos/preset_ecualizador.dart';
|
|
|
|
class ConfiguracionEcualizador {
|
|
const ConfiguracionEcualizador({
|
|
required this.principal,
|
|
required this.porEmisora,
|
|
this.activo = true,
|
|
this.eqMultiDeviceEnabled = false,
|
|
this.presetsDispositivo = const {},
|
|
this.presetsMatriz = const {},
|
|
});
|
|
|
|
final PresetEcualizador principal;
|
|
final Map<String, PresetEcualizador> porEmisora;
|
|
final bool activo;
|
|
|
|
/// Feature toggle: when false all multi-device logic is bypassed.
|
|
final bool eqMultiDeviceEnabled;
|
|
|
|
/// Per-device presets: deviceId → PresetEcualizador.
|
|
final Map<String, PresetEcualizador> presetsDispositivo;
|
|
|
|
/// Matrix presets: "stationUuid:deviceId" → PresetEcualizador.
|
|
final Map<String, PresetEcualizador> presetsMatriz;
|
|
}
|
|
|
|
class ServicioEcualizador {
|
|
ServicioEcualizador({SharedPreferences? prefs}) : _prefs = prefs;
|
|
|
|
static const _keyPresetPrincipal = 'eq_preset_principal_v1';
|
|
static const _keyPresetsPorEmisora = 'eq_presets_por_emisora_v1';
|
|
static const _keyActivo = 'eq_activo_v1';
|
|
static const _keyMultiDeviceEnabled = 'eq_multi_device_enabled_v1';
|
|
static const _keyPresetsPorDispositivo = 'eq_preset_por_dispositivo_v1';
|
|
static const _keyPresetsMatriz = 'eq_presets_matriz_v1';
|
|
|
|
final SharedPreferences? _prefs;
|
|
|
|
/// Injected startup instance (S3-R4); getInstance() is only a fallback.
|
|
Future<SharedPreferences> _resolverPrefs() async =>
|
|
_prefs ?? SharedPreferences.getInstance();
|
|
|
|
Future<ConfiguracionEcualizador> cargar() async {
|
|
final prefs = await _resolverPrefs();
|
|
final principal = _leerPresetPrincipal(prefs);
|
|
final porEmisora = _leerPresetsPorEmisora(prefs);
|
|
final presetsDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo);
|
|
final presetsMatriz = _leerMapa(prefs, _keyPresetsMatriz);
|
|
return ConfiguracionEcualizador(
|
|
principal: principal,
|
|
porEmisora: porEmisora,
|
|
activo: prefs.getBool(_keyActivo) ?? true,
|
|
eqMultiDeviceEnabled: prefs.getBool(_keyMultiDeviceEnabled) ?? false,
|
|
presetsDispositivo: presetsDispositivo,
|
|
presetsMatriz: presetsMatriz,
|
|
);
|
|
}
|
|
|
|
Future<void> guardarPrincipal(PresetEcualizador preset) async {
|
|
final prefs = await _resolverPrefs();
|
|
await prefs.setString(_keyPresetPrincipal, jsonEncode(preset.toJson()));
|
|
}
|
|
|
|
Future<void> guardarPorEmisora(String uuid, PresetEcualizador preset) async {
|
|
final prefs = await _resolverPrefs();
|
|
final mapa = _leerPresetsPorEmisora(prefs);
|
|
mapa[uuid] = preset;
|
|
await _guardarPresetsPorEmisora(prefs, mapa);
|
|
}
|
|
|
|
Future<void> guardarActivo(bool activo) async {
|
|
final prefs = await _resolverPrefs();
|
|
await prefs.setBool(_keyActivo, activo);
|
|
}
|
|
|
|
Future<void> eliminarPorEmisora(String uuid) async {
|
|
final prefs = await _resolverPrefs();
|
|
final mapa = _leerPresetsPorEmisora(prefs);
|
|
mapa.remove(uuid);
|
|
await _guardarPresetsPorEmisora(prefs, mapa);
|
|
}
|
|
|
|
Future<void> guardarConfiguracion(ConfiguracionEcualizador config) async {
|
|
final prefs = await _resolverPrefs();
|
|
await prefs.setString(
|
|
_keyPresetPrincipal,
|
|
jsonEncode(config.principal.toJson()),
|
|
);
|
|
await _guardarPresetsPorEmisora(prefs, config.porEmisora);
|
|
await prefs.setBool(_keyActivo, config.activo);
|
|
await prefs.setBool(_keyMultiDeviceEnabled, config.eqMultiDeviceEnabled);
|
|
await _guardarMapa(
|
|
prefs,
|
|
_keyPresetsPorDispositivo,
|
|
config.presetsDispositivo,
|
|
);
|
|
await _guardarMapa(prefs, _keyPresetsMatriz, config.presetsMatriz);
|
|
}
|
|
|
|
/// Persists the multi-device feature toggle.
|
|
Future<void> guardarToggleMultiDispositivo(bool habilitado) async {
|
|
final prefs = await _resolverPrefs();
|
|
await prefs.setBool(_keyMultiDeviceEnabled, habilitado);
|
|
}
|
|
|
|
/// Saves a per-device preset.
|
|
Future<void> guardarPresetDispositivo(
|
|
String deviceId,
|
|
PresetEcualizador preset,
|
|
) async {
|
|
final prefs = await _resolverPrefs();
|
|
final mapa = _leerMapa(prefs, _keyPresetsPorDispositivo);
|
|
mapa[deviceId] = preset;
|
|
await _guardarMapa(prefs, _keyPresetsPorDispositivo, mapa);
|
|
}
|
|
|
|
/// Saves a matrix (stationUuid:deviceId) preset entry.
|
|
Future<void> guardarPresetMatriz(
|
|
String clave,
|
|
PresetEcualizador preset,
|
|
) async {
|
|
final prefs = await _resolverPrefs();
|
|
final mapa = _leerMapa(prefs, _keyPresetsMatriz);
|
|
mapa[clave] = preset;
|
|
await _guardarMapa(prefs, _keyPresetsMatriz, mapa);
|
|
}
|
|
|
|
/// Removes a per-device preset entry.
|
|
Future<void> eliminarPresetDispositivo(String deviceId) async {
|
|
final prefs = await _resolverPrefs();
|
|
final mapa = _leerMapa(prefs, _keyPresetsPorDispositivo);
|
|
mapa.remove(deviceId);
|
|
await _guardarMapa(prefs, _keyPresetsPorDispositivo, mapa);
|
|
}
|
|
|
|
/// Removes a matrix preset entry.
|
|
Future<void> eliminarPresetMatriz(String clave) async {
|
|
final prefs = await _resolverPrefs();
|
|
final mapa = _leerMapa(prefs, _keyPresetsMatriz);
|
|
mapa.remove(clave);
|
|
await _guardarMapa(prefs, _keyPresetsMatriz, mapa);
|
|
}
|
|
|
|
/// Reads a `Map<String, PresetEcualizador>` from a SharedPreferences JSON key.
|
|
Map<String, PresetEcualizador> _leerMapa(
|
|
SharedPreferences prefs,
|
|
String key,
|
|
) {
|
|
final raw = prefs.getString(key);
|
|
if (raw == null || raw.isEmpty) return {};
|
|
try {
|
|
final data = Map<String, dynamic>.from(jsonDecode(raw) as Map);
|
|
return data.map(
|
|
(k, v) => MapEntry(
|
|
k,
|
|
PresetEcualizador.desdeJson(Map<String, dynamic>.from(v as Map)),
|
|
),
|
|
);
|
|
} catch (_) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
Future<void> _guardarMapa(
|
|
SharedPreferences prefs,
|
|
String key,
|
|
Map<String, PresetEcualizador> mapa,
|
|
) async {
|
|
final serializado = mapa.map((k, v) => MapEntry(k, v.toJson()));
|
|
await prefs.setString(key, jsonEncode(serializado));
|
|
}
|
|
|
|
PresetEcualizador _leerPresetPrincipal(SharedPreferences prefs) {
|
|
final raw = prefs.getString(_keyPresetPrincipal);
|
|
if (raw == null || raw.isEmpty) {
|
|
return PresetEcualizador.flat;
|
|
}
|
|
try {
|
|
return PresetEcualizador.desdeJson(
|
|
Map<String, dynamic>.from(jsonDecode(raw) as Map),
|
|
);
|
|
} catch (_) {
|
|
return PresetEcualizador.flat;
|
|
}
|
|
}
|
|
|
|
Map<String, PresetEcualizador> _leerPresetsPorEmisora(
|
|
SharedPreferences prefs,
|
|
) {
|
|
final raw = prefs.getString(_keyPresetsPorEmisora);
|
|
if (raw == null || raw.isEmpty) {
|
|
return {};
|
|
}
|
|
try {
|
|
final data = Map<String, dynamic>.from(jsonDecode(raw) as Map);
|
|
return data.map(
|
|
(uuid, preset) => MapEntry(
|
|
uuid,
|
|
PresetEcualizador.desdeJson(Map<String, dynamic>.from(preset as Map)),
|
|
),
|
|
);
|
|
} catch (_) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
Future<void> _guardarPresetsPorEmisora(
|
|
SharedPreferences prefs,
|
|
Map<String, PresetEcualizador> mapa,
|
|
) async {
|
|
final serializado = mapa.map(
|
|
(uuid, preset) => MapEntry(uuid, preset.toJson()),
|
|
);
|
|
await prefs.setString(_keyPresetsPorEmisora, jsonEncode(serializado));
|
|
}
|
|
}
|