feat(auto): add an Ecualizador browsable folder with preset selection

On-device feedback showed the equalizer's preset-cycling custom action
looked dead: many head units render custom actions icon-first, and a
monochrome icon cannot legibly encode "which of six presets" the way a
browsable list's text rows can.

This adds an "Ecualizador" folder to the car's browse tree, listing
"Desactivar" first, then the six factory presets by name, with the
currently-active one marked. Selecting a preset routes through the same
playFromMediaId seam every other browse-tree leaf already uses; picking
a preset while the equalizer is off turns it on and applies that preset.

Supersedes the earlier "no equalizer folder" rule (commit 2403da3),
which predated this feedback -- see decision auto/ecualizador-diseno.

The preset-cycling custom action still coexists with the folder in this
commit; it is removed in the next one.
This commit is contained in:
2026-07-31 19:11:01 +02:00
parent 25405564ee
commit 8423ccdd0c
31 changed files with 2686 additions and 2223 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,123 @@
import 'dart:ui' show Locale;
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/modelos/preset_ecualizador.dart';
import 'package:pluriwave/servicios/navegacion_auto.dart';
import 'package:pluriwave/servicios/servicio_audio.dart';
/// Decision `auto/ecualizador-diseno` -- the "Ecualizador" browsable
/// folder's item-building (`itemsEcualizadorAuto`), the pure,
/// `AppLocalizations`-dependent half of the folder feature. Lives in a
/// dedicated file, separate from `servicio_audio_eq_custom_actions_test.dart`
/// (which covers the now-playing screen's on/off toggle) because this is a
/// different car UI surface: a browsable folder, not a custom action.
void main() {
final l10n = lookupAppLocalizations(const Locale('es'));
group('itemsEcualizadorAuto (decision `auto/ecualizador-diseno`)', () {
test('devuelve exactamente 7 items: Desactivar primero, luego los 6 '
'presets de fábrica, todos playable', () {
final items = itemsEcualizadorAuto(
activo: true,
presetActual: PresetEcualizador.flat,
l10n: l10n,
);
expect(items, hasLength(7));
expect(items.first.id, ConstructorArbolAuto.idDesactivarEq);
for (final item in items) {
expect(item.playable, isTrue);
}
});
test('los 6 presets aparecen en el mismo orden que '
'PresetEcualizador.presets, cada uno con su id eq_preset:<nombre>', () {
final items = itemsEcualizadorAuto(
activo: true,
presetActual: PresetEcualizador.flat,
l10n: l10n,
);
final builder = ConstructorArbolAuto();
final idsPresets = items.skip(1).map((i) => i.id).toList();
final idsEsperados =
PresetEcualizador.presets
.map((p) => builder.idPresetEq(p.nombre))
.toList();
expect(idsPresets, idsEsperados);
});
test('los nombres de preset están localizados vía nombrePresetVisible, '
'no crudos', () {
final items = itemsEcualizadorAuto(
activo: true,
presetActual: PresetEcualizador.flat,
l10n: l10n,
);
final builder = ConstructorArbolAuto();
final rock = items.firstWhere(
(i) => i.id == builder.idPresetEq(PresetEcualizador.rock.nombre),
);
expect(rock.title, contains(nombrePresetVisible(l10n, 'Rock')));
});
test('con el ecualizador ACTIVO, el preset actual queda marcado y '
'Desactivar NO', () {
final items = itemsEcualizadorAuto(
activo: true,
presetActual: PresetEcualizador.jazz,
l10n: l10n,
);
final builder = ConstructorArbolAuto();
final desactivar = items.firstWhere(
(i) => i.id == ConstructorArbolAuto.idDesactivarEq,
);
final jazz = items.firstWhere(
(i) => i.id == builder.idPresetEq(PresetEcualizador.jazz.nombre),
);
final marcados = items.where((i) => i.title.contains('')).toList();
expect(desactivar.title, isNot(contains('')));
expect(jazz.title, contains(''));
expect(marcados, hasLength(1));
expect(marcados.single.id, jazz.id);
});
test('con el ecualizador DESACTIVADO, Desactivar queda marcado y NINGÚN '
'preset lo está', () {
final items = itemsEcualizadorAuto(
activo: false,
presetActual: PresetEcualizador.rock,
l10n: l10n,
);
final desactivar = items.firstWhere(
(i) => i.id == ConstructorArbolAuto.idDesactivarEq,
);
final marcados = items.where((i) => i.title.contains('')).toList();
expect(desactivar.title, contains(''));
expect(marcados, hasLength(1));
expect(marcados.single.id, ConstructorArbolAuto.idDesactivarEq);
});
test('el label de Desactivar usa l10n.autoEqDisableOption', () {
final items = itemsEcualizadorAuto(
activo: false,
presetActual: PresetEcualizador.flat,
l10n: l10n,
);
final desactivar = items.firstWhere(
(i) => i.id == ConstructorArbolAuto.idDesactivarEq,
);
expect(desactivar.title, contains(l10n.autoEqDisableOption));
});
});
}