feat(auto): equalizer folder in the browse tree, one toggle on playback
On-device feedback: two identical icons on the car's now-playing screen, one of which looked dead. It worked -- but head units render custom actions icon-first, so cycling six presets behind one static glyph was invisible. A monochrome icon cannot encode which of six presets is active. Android Auto separates the idioms deliberately: custom actions for stateless toggles, browsable lists for choosing among options. - Playback screen keeps one action: equalizer on/off, state-aware icons - New Ecualizador folder lists Desactivar plus the six presets by name, active one marked - The preset-cycling action and its drawable are removed Supersedes the redesign's no-equalizer-folder rule, which predated knowing custom actions do not surface state in a car. # Conflicts: # lib/l10n/app_ar.arb # lib/l10n/app_bn.arb # lib/l10n/app_de.arb # lib/l10n/app_en.arb # lib/l10n/app_es.arb # lib/l10n/app_fr.arb # lib/l10n/app_hi.arb # lib/l10n/app_id.arb # lib/l10n/app_it.arb # lib/l10n/app_ja.arb # lib/l10n/app_pt.arb # lib/l10n/app_ru.arb # lib/l10n/app_zh.arb
This commit is contained in:
+2213
-2154
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui' show Locale;
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
@@ -19,14 +20,8 @@ void main() {
|
||||
|
||||
group('presetSiguiente (item 4 — cycling presets)', () {
|
||||
test('advances to the next preset in order', () {
|
||||
expect(
|
||||
presetSiguiente(PresetEcualizador.flat),
|
||||
PresetEcualizador.rock,
|
||||
);
|
||||
expect(
|
||||
presetSiguiente(PresetEcualizador.rock),
|
||||
PresetEcualizador.pop,
|
||||
);
|
||||
expect(presetSiguiente(PresetEcualizador.flat), PresetEcualizador.rock);
|
||||
expect(presetSiguiente(PresetEcualizador.rock), PresetEcualizador.pop);
|
||||
});
|
||||
|
||||
test('wraps around after the last preset', () {
|
||||
@@ -36,21 +31,15 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'an unknown/custom preset (e.g. a user-tweaked "Personalizado" band '
|
||||
'set) starts from the FIRST preset instead of throwing',
|
||||
() {
|
||||
final personalizado = PresetEcualizador(
|
||||
nombre: 'Personalizado',
|
||||
bandas: [1.0, 2.0, 3.0, 4.0, 5.0],
|
||||
);
|
||||
test('an unknown/custom preset (e.g. a user-tweaked "Personalizado" band '
|
||||
'set) starts from the FIRST preset instead of throwing', () {
|
||||
final personalizado = PresetEcualizador(
|
||||
nombre: 'Personalizado',
|
||||
bandas: [1.0, 2.0, 3.0, 4.0, 5.0],
|
||||
);
|
||||
|
||||
expect(
|
||||
presetSiguiente(personalizado),
|
||||
PresetEcualizador.presets.first,
|
||||
);
|
||||
},
|
||||
);
|
||||
expect(presetSiguiente(personalizado), PresetEcualizador.presets.first);
|
||||
});
|
||||
|
||||
test('respects an injected presets list instead of the default 6', () {
|
||||
final propios = [PresetEcualizador.jazz, PresetEcualizador.voz];
|
||||
@@ -84,7 +73,10 @@ void main() {
|
||||
});
|
||||
|
||||
test('an unrecognized name falls through verbatim', () {
|
||||
expect(nombrePresetVisible(l10n, 'Mi Preset Guardado'), 'Mi Preset Guardado');
|
||||
expect(
|
||||
nombrePresetVisible(l10n, 'Mi Preset Guardado'),
|
||||
'Mi Preset Guardado',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,35 +85,32 @@ void main() {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
disponible: false,
|
||||
activo: true,
|
||||
presetActual: PresetEcualizador.flat,
|
||||
l10n: l10n,
|
||||
);
|
||||
|
||||
expect(controles, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'exactly 2 custom actions when available: on/off toggle + preset '
|
||||
'cycle -- Android Auto shows a limited number of custom actions, so '
|
||||
'this is deliberately NOT one action per preset',
|
||||
() {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
disponible: true,
|
||||
activo: true,
|
||||
presetActual: PresetEcualizador.rock,
|
||||
l10n: l10n,
|
||||
);
|
||||
test('exactly 1 custom action when available: the on/off toggle -- '
|
||||
'decision `auto/ecualizador-diseno` REMOVES the preset-cycling '
|
||||
'action that used to sit alongside it; preset selection now lives '
|
||||
'in the "Ecualizador" browsable folder instead (see '
|
||||
'`itemsEcualizadorAuto`)', () {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
disponible: true,
|
||||
activo: true,
|
||||
l10n: l10n,
|
||||
);
|
||||
|
||||
expect(controles, hasLength(2));
|
||||
expect(controles.every((c) => c.action == MediaAction.custom), isTrue);
|
||||
},
|
||||
);
|
||||
expect(controles, hasLength(1));
|
||||
expect(controles.single.action, MediaAction.custom);
|
||||
expect(controles.single.customAction?.name, accionEqToggle);
|
||||
});
|
||||
|
||||
test('toggle label reflects ON -> shows "disable" action', () {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
disponible: true,
|
||||
activo: true,
|
||||
presetActual: PresetEcualizador.flat,
|
||||
l10n: l10n,
|
||||
);
|
||||
final toggle = controles.firstWhere(
|
||||
@@ -135,7 +124,6 @@ void main() {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
disponible: true,
|
||||
activo: false,
|
||||
presetActual: PresetEcualizador.flat,
|
||||
l10n: l10n,
|
||||
);
|
||||
final toggle = controles.firstWhere(
|
||||
@@ -145,45 +133,68 @@ void main() {
|
||||
expect(toggle.label, l10n.eqCustomActionEnableLabel);
|
||||
});
|
||||
|
||||
test('preset-cycle label shows the CURRENT preset localized name', () {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
test('toggle icon reflects EQ state: ON uses ic_auto_eq_on, OFF uses '
|
||||
'ic_auto_eq_off -- a car head unit foregrounds the icon over the '
|
||||
'label, so the icon itself must change, not just the text', () {
|
||||
final activado = controlesEcualizadorPersonalizados(
|
||||
disponible: true,
|
||||
activo: true,
|
||||
presetActual: PresetEcualizador.jazz,
|
||||
l10n: l10n,
|
||||
);
|
||||
final ciclo = controles.firstWhere(
|
||||
(c) => c.customAction?.name == accionEqPresetSiguiente,
|
||||
);
|
||||
|
||||
expect(
|
||||
ciclo.label,
|
||||
l10n.eqCustomActionPresetLabel(l10n.equalizerPresetJazz),
|
||||
);
|
||||
});
|
||||
|
||||
test('both actions reuse the bundled notification drawable (zero new '
|
||||
'native assets)', () {
|
||||
final controles = controlesEcualizadorPersonalizados(
|
||||
).firstWhere((c) => c.customAction?.name == accionEqToggle);
|
||||
final desactivado = controlesEcualizadorPersonalizados(
|
||||
disponible: true,
|
||||
activo: true,
|
||||
presetActual: PresetEcualizador.flat,
|
||||
activo: false,
|
||||
l10n: l10n,
|
||||
);
|
||||
).firstWhere((c) => c.customAction?.name == accionEqToggle);
|
||||
|
||||
expect(
|
||||
controles.every((c) => c.androidIcon == 'drawable/ic_stat_pluriwave'),
|
||||
isTrue,
|
||||
);
|
||||
expect(activado.androidIcon, 'drawable/ic_auto_eq_on');
|
||||
expect(desactivado.androidIcon, 'drawable/ic_auto_eq_off');
|
||||
});
|
||||
});
|
||||
|
||||
group('action name constants (item 4 — collision-free with car-tree ids)', () {
|
||||
test('names are non-empty, distinct, and do not collide with any '
|
||||
'existing browse-tree media-id prefix', () {
|
||||
expect(accionEqToggle, isNotEmpty);
|
||||
expect(accionEqPresetSiguiente, isNotEmpty);
|
||||
expect(accionEqToggle, isNot(equals(accionEqPresetSiguiente)));
|
||||
group(
|
||||
'action name constants (item 4 -- collision-free with car-tree ids)',
|
||||
() {
|
||||
test('accionEqToggle is non-empty and does not collide with any '
|
||||
'existing browse-tree media-id prefix -- accionEqPresetSiguiente '
|
||||
'(decision `auto/ecualizador-diseno`: removed, superseded by the '
|
||||
'"Ecualizador" browsable folder) no longer exists as a symbol at '
|
||||
'all, which this file compiling proves on its own', () {
|
||||
expect(accionEqToggle, isNotEmpty);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group('equalizer drawable assets on disk (on-device feedback follow-up: the '
|
||||
'two custom actions used to share one drawable and were visually '
|
||||
'indistinguishable)', () {
|
||||
test('ic_auto_eq_on and ic_auto_eq_off exist under '
|
||||
'android/app/src/main/res/drawable/ -- a missing drawable is not a '
|
||||
'build error, it silently renders blank/default on the head unit, '
|
||||
'so this is the only safety net that would have caught the '
|
||||
'original duplication. ic_auto_eq_preset is deliberately NOT '
|
||||
'checked here anymore -- decision `auto/ecualizador-diseno` '
|
||||
'removes the preset-cycling action and its drawable', () {
|
||||
for (final nombre in ['ic_auto_eq_on', 'ic_auto_eq_off']) {
|
||||
final archivo = File('android/app/src/main/res/drawable/$nombre.xml');
|
||||
expect(
|
||||
archivo.existsSync(),
|
||||
isTrue,
|
||||
reason:
|
||||
'$nombre.xml must exist under '
|
||||
'android/app/src/main/res/drawable/',
|
||||
);
|
||||
}
|
||||
expect(
|
||||
File(
|
||||
'android/app/src/main/res/drawable/ic_auto_eq_preset.xml',
|
||||
).existsSync(),
|
||||
isFalse,
|
||||
reason:
|
||||
'ic_auto_eq_preset.xml must be REMOVED -- decision '
|
||||
'`auto/ecualizador-diseno` retires the preset-cycling '
|
||||
'custom action it belonged to',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user