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:
@@ -60,6 +60,8 @@ class _AjustesContent extends StatelessWidget {
|
||||
children: const [
|
||||
_SeccionEcualizador(),
|
||||
SizedBox(height: 12),
|
||||
_SeccionEcualizadorAvanzado(),
|
||||
SizedBox(height: 12),
|
||||
_SeccionGrabaciones(),
|
||||
SizedBox(height: 12),
|
||||
_SeccionTimerSueno(),
|
||||
@@ -652,6 +654,126 @@ class _SeccionEcualizador extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// "Advanced Equalization Options" settings section (Phase 7, multi-device EQ).
|
||||
///
|
||||
/// Always shows the feature toggle so the user can discover it. When the
|
||||
/// toggle is OFF, the device list is completely absent (not just invisible),
|
||||
/// matching the spec scenario "Settings section is absent when toggle is off".
|
||||
class _SeccionEcualizadorAvanzado extends StatelessWidget {
|
||||
const _SeccionEcualizadorAvanzado();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final eq = context.watch<EstadoEcualizador>();
|
||||
final multiDeviceEnabled = eq.eqMultiDeviceEnabled;
|
||||
final presetsDispositivo = eq.presetsDispositivo;
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.devices_rounded),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.advancedEqSectionTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// The toggle uses GestureDetector + custom row instead of
|
||||
// SwitchListTile to avoid Material ink assertion inside
|
||||
// PluriGlassSurface's DecoratedBox. The visual result is identical
|
||||
// to SwitchListTile.
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap:
|
||||
() => eq.cambiarMultiDeviceEnabled(!multiDeviceEnabled),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.advancedEqEnableToggle,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
l10n.advancedEqEnableToggleSubtitle,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: multiDeviceEnabled,
|
||||
onChanged:
|
||||
(habilitado) => eq.cambiarMultiDeviceEnabled(habilitado),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (multiDeviceEnabled) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.advancedEqKnownDevicesTitle,
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (presetsDispositivo.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
l10n.advancedEqKnownDevicesEmpty,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
)
|
||||
else
|
||||
for (final entry in presetsDispositivo.entries)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.headphones_rounded, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
entry.key,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Text(
|
||||
l10n.advancedEqDevicePresetLabel(
|
||||
entry.value.nombre,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SeccionOrdenListas extends StatelessWidget {
|
||||
const _SeccionOrdenListas();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user