fix(ecualizador): header switch, banner radius, preset chips, glow thumb
Audit 11.1 (t4:566): the master enable switch moves to PluriPushScaffold's header actions -- was the body's first SwitchListTile row. Its realtime/pending explainer subtitle stays behind as a plain caption so no information is lost. Audit 11.2 (t4:571): explainer banner radius is 16 (a local one-off, matching neither of the 3 named tokens), not radiusSm's 14. Audit 11.3 (t4:574-577): preset chips are solid brand-teal with dark text when active, listSurface + a faint border when not -- was Material's own ChoiceChip theming (primaryContainer/grey). Audit 11.5 (t4:585): band sliders use a new 20x20 _GlowSliderThumbShape (brand-teal blurred glow + solid thumb), replacing the Material default round thumb. Audit 11.7 (t4:584): the dB label is brand teal at 90% alpha, not liveGreen -- a leftover wrong colour family 11.6's slider-only fix never touched. Updated 2 pre-existing tests (pantalla_ajustes_ecualizador_test.dart, pantalla_ajustes_test.dart) to locate the enable switch by key instead of by the "Enable equalizer" text it no longer renders next to.
This commit is contained in:
@@ -8,6 +8,7 @@ import '../../estado/estado_radio.dart';
|
|||||||
import '../../l10n/gen/app_localizations.dart';
|
import '../../l10n/gen/app_localizations.dart';
|
||||||
import '../../modelos/preset_ecualizador.dart';
|
import '../../modelos/preset_ecualizador.dart';
|
||||||
import '../../tema/pluriwave_theme.dart';
|
import '../../tema/pluriwave_theme.dart';
|
||||||
|
import '../../tema/pluriwave_tokens.dart';
|
||||||
import '../../widgets/ecualizador_widget.dart';
|
import '../../widgets/ecualizador_widget.dart';
|
||||||
import '../../widgets/pluri_glass_surface.dart';
|
import '../../widgets/pluri_glass_surface.dart';
|
||||||
import '../../widgets/pluri_layout.dart';
|
import '../../widgets/pluri_layout.dart';
|
||||||
@@ -36,13 +37,33 @@ class PantallaAjustesEcualizador extends StatelessWidget {
|
|||||||
const PantallaAjustesEcualizador({super.key});
|
const PantallaAjustesEcualizador({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => PluriPushScaffold(
|
Widget build(BuildContext context) {
|
||||||
title: AppLocalizations.of(context).equalizerTitle,
|
// Audit 11.1 (t4 line 566): the master enable switch lives in the
|
||||||
body: ListView(
|
// HEADER, not as the body's first row -- read here (a second,
|
||||||
padding: PluriLayout.pageContentPadding,
|
// cheap watch alongside _CuerpoEcualizadorState's own Consumer2)
|
||||||
children: const [_CuerpoEcualizador()],
|
// purely to feed PluriPushScaffold.actions.
|
||||||
),
|
final eq = context.watch<EstadoEcualizador>();
|
||||||
);
|
return PluriPushScaffold(
|
||||||
|
title: AppLocalizations.of(context).equalizerTitle,
|
||||||
|
actions: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: Switch(
|
||||||
|
key: const ValueKey('eq-master-switch'),
|
||||||
|
value: eq.activo,
|
||||||
|
onChanged: eq.cambiarActivo,
|
||||||
|
// t4 line 569: brand-teal track, white thumb -- the default
|
||||||
|
// Material thumb colour already renders white when "on".
|
||||||
|
activeTrackColor: PluriWaveTokens.brand,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body: ListView(
|
||||||
|
padding: PluriLayout.pageContentPadding,
|
||||||
|
children: const [_CuerpoEcualizador()],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CuerpoEcualizador extends StatefulWidget {
|
class _CuerpoEcualizador extends StatefulWidget {
|
||||||
@@ -83,17 +104,18 @@ class _CuerpoEcualizadorState extends State<_CuerpoEcualizador> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
SwitchListTile.adaptive(
|
// Audit 11.1 (t4 line 566): the enable switch itself now lives
|
||||||
contentPadding: EdgeInsets.zero,
|
// in PluriPushScaffold's header (see PantallaAjustesEcualizador
|
||||||
title: Text(l10n.equalizerEnable),
|
// above) -- this stays behind only as the explanatory caption
|
||||||
subtitle: Text(
|
// the old SwitchListTile's subtitle carried, so that
|
||||||
disponible
|
// information is not lost.
|
||||||
? l10n.equalizerRealtimeSubtitle
|
Text(
|
||||||
: l10n.equalizerPendingSubtitle,
|
disponible
|
||||||
),
|
? l10n.equalizerRealtimeSubtitle
|
||||||
value: eq.activo,
|
: l10n.equalizerPendingSubtitle,
|
||||||
onChanged: eq.cambiarActivo,
|
style: Theme.of(ctx).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
if (mostrarModoPorEmisora) ...[
|
if (mostrarModoPorEmisora) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
SwitchListTile.adaptive(
|
SwitchListTile.adaptive(
|
||||||
@@ -164,6 +186,12 @@ class _BannerExplicacionBase extends StatelessWidget {
|
|||||||
|
|
||||||
final AppLocalizations l10n;
|
final AppLocalizations l10n;
|
||||||
|
|
||||||
|
/// Audit 11.2 (t4 line 571): 16 -- doesn't match any of
|
||||||
|
/// [PluriWaveTokens]'s three named radii (14/18/30), so this stays a
|
||||||
|
/// local one-off constant (same precedent as `_stopButtonRadius` in
|
||||||
|
/// `pantalla_alarma_sonando.dart`).
|
||||||
|
static const _bannerRadius = 16.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tokens = context.pluriTokens;
|
final tokens = context.pluriTokens;
|
||||||
@@ -172,7 +200,7 @@ class _BannerExplicacionBase extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: tokens.liveGreen.withValues(alpha: 0.09),
|
color: tokens.liveGreen.withValues(alpha: 0.09),
|
||||||
borderRadius: BorderRadius.circular(tokens.radiusSm),
|
borderRadius: BorderRadius.circular(_bannerRadius),
|
||||||
border: Border.all(color: tokens.liveGreen.withValues(alpha: 0.26)),
|
border: Border.all(color: tokens.liveGreen.withValues(alpha: 0.26)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
|
|||||||
theme.colorScheme.surfaceContainerHighest,
|
theme.colorScheme.surfaceContainerHighest,
|
||||||
overlayColor: PluriWaveTokens.brand
|
overlayColor: PluriWaveTokens.brand
|
||||||
.withValues(alpha: 0.15),
|
.withValues(alpha: 0.15),
|
||||||
|
// Audit 11.5 (t4 line 585): a 20x20 thumb
|
||||||
|
// with a 14px brand-teal glow -- was the
|
||||||
|
// Material default round thumb shape.
|
||||||
|
thumbShape: const _GlowSliderThumbShape(),
|
||||||
),
|
),
|
||||||
child: Slider(
|
child: Slider(
|
||||||
value: _bandas[i],
|
value: _bandas[i],
|
||||||
@@ -130,8 +134,12 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${_bandas[i].toStringAsFixed(1)}dB',
|
'${_bandas[i].toStringAsFixed(1)}dB',
|
||||||
|
// Audit 11.7 (t4 line 584): the prototype's dB
|
||||||
|
// label is brand teal at 90% alpha -- `liveGreen`
|
||||||
|
// is the LIVE-badge colour, an unrelated wrong
|
||||||
|
// family untouched by 11.6's slider-only fix.
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: tokens.liveGreen,
|
color: PluriWaveTokens.brand.withValues(alpha: 0.9),
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -154,6 +162,50 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Audit 11.5 (t4 line 585): `width:6px;border-radius:3px` track with a
|
||||||
|
/// `20x20` thumb carrying `box-shadow:0 0 14px rgba(33,212,217,.6)` --
|
||||||
|
/// Material's stock `RoundSliderThumbShape` has neither the exact size nor
|
||||||
|
/// a coloured glow (its own elevation shadow is a neutral drop shadow, not
|
||||||
|
/// brand-tinted). Paints a soft blurred glow first, then the solid thumb
|
||||||
|
/// on top, both centred on the slider's reported thumb position.
|
||||||
|
class _GlowSliderThumbShape extends SliderComponentShape {
|
||||||
|
const _GlowSliderThumbShape();
|
||||||
|
|
||||||
|
static const _radius = 10.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size getPreferredSize(bool isEnabled, bool isDiscrete) =>
|
||||||
|
const Size(_radius * 2, _radius * 2);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(
|
||||||
|
PaintingContext context,
|
||||||
|
Offset center, {
|
||||||
|
required Animation<double> activationAnimation,
|
||||||
|
required Animation<double> enableAnimation,
|
||||||
|
required bool isDiscrete,
|
||||||
|
required TextPainter labelPainter,
|
||||||
|
required RenderBox parentBox,
|
||||||
|
required SliderThemeData sliderTheme,
|
||||||
|
required TextDirection textDirection,
|
||||||
|
required double value,
|
||||||
|
required double textScaleFactor,
|
||||||
|
required Size sizeWithOverflow,
|
||||||
|
}) {
|
||||||
|
final canvas = context.canvas;
|
||||||
|
final color = sliderTheme.thumbColor ?? PluriWaveTokens.brand;
|
||||||
|
|
||||||
|
final glowPaint =
|
||||||
|
Paint()
|
||||||
|
..color = color.withValues(alpha: 0.6)
|
||||||
|
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 7);
|
||||||
|
canvas.drawCircle(center, _radius + 4, glowPaint);
|
||||||
|
|
||||||
|
final thumbPaint = Paint()..color = color;
|
||||||
|
canvas.drawCircle(center, _radius, thumbPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String _nombrePreset(AppLocalizations l10n, String nombre) {
|
String _nombrePreset(AppLocalizations l10n, String nombre) {
|
||||||
return switch (nombre) {
|
return switch (nombre) {
|
||||||
'Flat' => l10n.equalizerPresetFlat,
|
'Flat' => l10n.equalizerPresetFlat,
|
||||||
@@ -188,7 +240,6 @@ class PresetsEcualizadorWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final todos = [...PresetEcualizador.presets, ...personalizados];
|
final todos = [...PresetEcualizador.presets, ...personalizados];
|
||||||
return Wrap(
|
return Wrap(
|
||||||
@@ -197,13 +248,32 @@ class PresetsEcualizadorWidget extends StatelessWidget {
|
|||||||
children:
|
children:
|
||||||
todos.map((p) {
|
todos.map((p) {
|
||||||
final selected = p.nombre == presetActual.nombre;
|
final selected = p.nombre == presetActual.nombre;
|
||||||
|
// Audit 11.3 (t4 lines 574-577): a solid brand-teal chip with
|
||||||
|
// dark text when active, `listSurface` + a faint border when
|
||||||
|
// not -- was Material's own `ChoiceChip` theming
|
||||||
|
// (`primaryContainer` selected / translucent grey unselected).
|
||||||
return ChoiceChip(
|
return ChoiceChip(
|
||||||
label: Text(_nombrePreset(l10n, p.nombre)),
|
label: Text(_nombrePreset(l10n, p.nombre)),
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color:
|
||||||
|
selected
|
||||||
|
? const Color(0xFF062126)
|
||||||
|
: const Color(0xFFF2F7FA),
|
||||||
|
),
|
||||||
selected: selected,
|
selected: selected,
|
||||||
showCheckmark: false,
|
showCheckmark: false,
|
||||||
selectedColor: theme.colorScheme.primaryContainer,
|
selectedColor: PluriWaveTokens.brand,
|
||||||
backgroundColor: theme.colorScheme.surfaceContainerHighest
|
backgroundColor: PluriWaveTokens.dark.listSurface,
|
||||||
.withValues(alpha: 0.32),
|
side: BorderSide(
|
||||||
|
color:
|
||||||
|
selected
|
||||||
|
? Colors.transparent
|
||||||
|
: Colors.white.withValues(alpha: 0.09),
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
onSelected: (_) => onSeleccionar(p),
|
onSelected: (_) => onSeleccionar(p),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:pluriwave/modelos/dispositivo_audio.dart';
|
|||||||
import 'package:pluriwave/modelos/emisora.dart';
|
import 'package:pluriwave/modelos/emisora.dart';
|
||||||
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||||
import 'package:pluriwave/pantallas/ajustes/pantalla_ajustes_ecualizador.dart';
|
import 'package:pluriwave/pantallas/ajustes/pantalla_ajustes_ecualizador.dart';
|
||||||
|
import 'package:pluriwave/tema/pluriwave_tokens.dart';
|
||||||
import 'package:pluriwave/widgets/ecualizador_widget.dart';
|
import 'package:pluriwave/widgets/ecualizador_widget.dart';
|
||||||
import 'package:pluriwave/widgets/pluri_glass_surface.dart';
|
import 'package:pluriwave/widgets/pluri_glass_surface.dart';
|
||||||
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
|
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
|
||||||
@@ -119,9 +120,8 @@ void main() {
|
|||||||
expect((appBar.title as Text).data, equals('Equalizer'));
|
expect((appBar.title as Text).data, equals('Equalizer'));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('moved control still responds: enable switch toggles activo', (
|
testWidgets('moved control still responds: enable switch toggles activo '
|
||||||
tester,
|
'(audit 11.1: now in the header, t4 line 566)', (tester) async {
|
||||||
) async {
|
|
||||||
_suppressListTileInkAssertion();
|
_suppressListTileInkAssertion();
|
||||||
final estado = await crearEstado();
|
final estado = await crearEstado();
|
||||||
addTearDown(estado.dispose);
|
addTearDown(estado.dispose);
|
||||||
@@ -130,7 +130,7 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final before = estado.ecualizador.activo;
|
final before = estado.ecualizador.activo;
|
||||||
await tester.tap(find.text('Enable equalizer'));
|
await tester.tap(find.byKey(const ValueKey('eq-master-switch')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(estado.ecualizador.activo, equals(!before));
|
expect(estado.ecualizador.activo, equals(!before));
|
||||||
@@ -447,6 +447,74 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'visual fidelity (audit 11.1): the master switch is a header action, '
|
||||||
|
'not the body\'s first row (t4 line 566)',
|
||||||
|
(tester) async {
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
final estado = await crearEstado();
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(AppBar),
|
||||||
|
matching: find.byKey(const ValueKey('eq-master-switch')),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
reason: 'the master switch lives in the AppBar, not the body',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(ListView),
|
||||||
|
matching: find.byKey(const ValueKey('eq-master-switch')),
|
||||||
|
),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'visual fidelity (audit 11.2): the explainer banner radius is 16, '
|
||||||
|
'not radiusSm\'s 14 (t4 line 571)',
|
||||||
|
(tester) async {
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
final estado = await crearEstado();
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final banner = tester.widget<Container>(
|
||||||
|
find.byKey(const Key('eq-base-explainer-banner')),
|
||||||
|
);
|
||||||
|
final decoration = banner.decoration as BoxDecoration;
|
||||||
|
expect(
|
||||||
|
(decoration.borderRadius as BorderRadius).topLeft,
|
||||||
|
const Radius.circular(16),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('visual fidelity (audit 11.3): the active preset chip is solid '
|
||||||
|
'brand teal with dark text (t4 lines 574-577)', (tester) async {
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
final estado = await crearEstado();
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final chip = tester.widget<ChoiceChip>(
|
||||||
|
find.widgetWithText(ChoiceChip, 'Flat'),
|
||||||
|
);
|
||||||
|
expect(chip.selected, isTrue);
|
||||||
|
expect(chip.selectedColor, PluriWaveTokens.brand);
|
||||||
|
expect(chip.labelStyle?.color, const Color(0xFF062126));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'Guardar como preset with a whitespace-only name shows the same validation message',
|
'Guardar como preset with a whitespace-only name shows the same validation message',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
|
|||||||
@@ -162,9 +162,10 @@ void main() {
|
|||||||
await pumpStable(tester);
|
await pumpStable(tester);
|
||||||
|
|
||||||
// Pushed, not index-switched: exactly one PluriPushScaffold now exists,
|
// Pushed, not index-switched: exactly one PluriPushScaffold now exists,
|
||||||
// and its moved control (the enable switch) is reachable.
|
// and its moved control (the enable switch, audit 11.1 -- now a
|
||||||
|
// header action, t4 line 566) is reachable.
|
||||||
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
||||||
expect(find.text('Enable equalizer'), findsOneWidget);
|
expect(find.byKey(const ValueKey('eq-master-switch')), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('tapping the Orden de listas row pushes its detail screen', (
|
testWidgets('tapping the Orden de listas row pushes its detail screen', (
|
||||||
|
|||||||
@@ -136,4 +136,30 @@ void main() {
|
|||||||
'EQ bands with brand teal #21D4D9 (t4 line 585)',
|
'EQ bands with brand teal #21D4D9 (t4 line 585)',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'visual fidelity (audit 11.5): the thumb is a 20x20 custom glow shape, '
|
||||||
|
'not the Material default (t4 line 585)',
|
||||||
|
(tester) async {
|
||||||
|
await tester.pumpWidget(buildWidget());
|
||||||
|
|
||||||
|
final tema = SliderTheme.of(tester.element(find.byType(Slider).first));
|
||||||
|
final size = tema.thumbShape?.getPreferredSize(true, false);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
size,
|
||||||
|
const Size(20, 20),
|
||||||
|
reason: 't4 line 585: a 20x20 thumb with a 14px brand-teal glow',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('visual fidelity (audit 11.7): the dB label is brand teal, not '
|
||||||
|
'liveGreen (t4 line 584)', (tester) async {
|
||||||
|
await tester.pumpWidget(buildWidget());
|
||||||
|
|
||||||
|
final label = tester.widget<Text>(find.text('0.0dB').first);
|
||||||
|
expect(label.style?.color, PluriWaveTokens.brand.withValues(alpha: 0.9));
|
||||||
|
expect(label.style?.color, isNot(PluriWaveTokens.dark.liveGreen));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user