diff --git a/lib/pantallas/ajustes/pantalla_ajustes_ecualizador.dart b/lib/pantallas/ajustes/pantalla_ajustes_ecualizador.dart index b899295..f4c658f 100644 --- a/lib/pantallas/ajustes/pantalla_ajustes_ecualizador.dart +++ b/lib/pantallas/ajustes/pantalla_ajustes_ecualizador.dart @@ -8,6 +8,7 @@ import '../../estado/estado_radio.dart'; import '../../l10n/gen/app_localizations.dart'; import '../../modelos/preset_ecualizador.dart'; import '../../tema/pluriwave_theme.dart'; +import '../../tema/pluriwave_tokens.dart'; import '../../widgets/ecualizador_widget.dart'; import '../../widgets/pluri_glass_surface.dart'; import '../../widgets/pluri_layout.dart'; @@ -36,13 +37,33 @@ class PantallaAjustesEcualizador extends StatelessWidget { const PantallaAjustesEcualizador({super.key}); @override - Widget build(BuildContext context) => PluriPushScaffold( - title: AppLocalizations.of(context).equalizerTitle, - body: ListView( - padding: PluriLayout.pageContentPadding, - children: const [_CuerpoEcualizador()], - ), - ); + Widget build(BuildContext context) { + // Audit 11.1 (t4 line 566): the master enable switch lives in the + // HEADER, not as the body's first row -- read here (a second, + // cheap watch alongside _CuerpoEcualizadorState's own Consumer2) + // purely to feed PluriPushScaffold.actions. + final eq = context.watch(); + 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 { @@ -83,17 +104,18 @@ class _CuerpoEcualizadorState extends State<_CuerpoEcualizador> { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SwitchListTile.adaptive( - contentPadding: EdgeInsets.zero, - title: Text(l10n.equalizerEnable), - subtitle: Text( - disponible - ? l10n.equalizerRealtimeSubtitle - : l10n.equalizerPendingSubtitle, - ), - value: eq.activo, - onChanged: eq.cambiarActivo, + // Audit 11.1 (t4 line 566): the enable switch itself now lives + // in PluriPushScaffold's header (see PantallaAjustesEcualizador + // above) -- this stays behind only as the explanatory caption + // the old SwitchListTile's subtitle carried, so that + // information is not lost. + Text( + disponible + ? l10n.equalizerRealtimeSubtitle + : l10n.equalizerPendingSubtitle, + style: Theme.of(ctx).textTheme.bodySmall, ), + const SizedBox(height: 12), if (mostrarModoPorEmisora) ...[ const SizedBox(height: 8), SwitchListTile.adaptive( @@ -164,6 +186,12 @@ class _BannerExplicacionBase extends StatelessWidget { 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 Widget build(BuildContext context) { final tokens = context.pluriTokens; @@ -172,7 +200,7 @@ class _BannerExplicacionBase extends StatelessWidget { padding: const EdgeInsets.all(14), decoration: BoxDecoration( 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)), ), child: Row( diff --git a/lib/widgets/ecualizador_widget.dart b/lib/widgets/ecualizador_widget.dart index 267ff62..50aac40 100644 --- a/lib/widgets/ecualizador_widget.dart +++ b/lib/widgets/ecualizador_widget.dart @@ -113,6 +113,10 @@ class _EcualizadorWidgetState extends State { theme.colorScheme.surfaceContainerHighest, overlayColor: PluriWaveTokens.brand .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( value: _bandas[i], @@ -130,8 +134,12 @@ class _EcualizadorWidgetState extends State { ), Text( '${_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( - color: tokens.liveGreen, + color: PluriWaveTokens.brand.withValues(alpha: 0.9), fontWeight: FontWeight.w700, ), ), @@ -154,6 +162,50 @@ class _EcualizadorWidgetState extends State { } } +/// 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 activationAnimation, + required Animation 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) { return switch (nombre) { 'Flat' => l10n.equalizerPresetFlat, @@ -188,7 +240,6 @@ class PresetsEcualizadorWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); final l10n = AppLocalizations.of(context); final todos = [...PresetEcualizador.presets, ...personalizados]; return Wrap( @@ -197,13 +248,32 @@ class PresetsEcualizadorWidget extends StatelessWidget { children: todos.map((p) { 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( label: Text(_nombrePreset(l10n, p.nombre)), + labelStyle: TextStyle( + fontWeight: FontWeight.w800, + color: + selected + ? const Color(0xFF062126) + : const Color(0xFFF2F7FA), + ), selected: selected, showCheckmark: false, - selectedColor: theme.colorScheme.primaryContainer, - backgroundColor: theme.colorScheme.surfaceContainerHighest - .withValues(alpha: 0.32), + selectedColor: PluriWaveTokens.brand, + backgroundColor: PluriWaveTokens.dark.listSurface, + side: BorderSide( + color: + selected + ? Colors.transparent + : Colors.white.withValues(alpha: 0.09), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), onSelected: (_) => onSeleccionar(p), ); }).toList(), diff --git a/test/pantallas/ajustes/pantalla_ajustes_ecualizador_test.dart b/test/pantallas/ajustes/pantalla_ajustes_ecualizador_test.dart index d8c58ea..c4f5024 100644 --- a/test/pantallas/ajustes/pantalla_ajustes_ecualizador_test.dart +++ b/test/pantallas/ajustes/pantalla_ajustes_ecualizador_test.dart @@ -9,6 +9,7 @@ import 'package:pluriwave/modelos/dispositivo_audio.dart'; import 'package:pluriwave/modelos/emisora.dart'; import 'package:pluriwave/modelos/preset_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/pluri_glass_surface.dart'; import 'package:pluriwave/widgets/pluri_push_scaffold.dart'; @@ -119,9 +120,8 @@ void main() { expect((appBar.title as Text).data, equals('Equalizer')); }); - testWidgets('moved control still responds: enable switch toggles activo', ( - tester, - ) async { + testWidgets('moved control still responds: enable switch toggles activo ' + '(audit 11.1: now in the header, t4 line 566)', (tester) async { _suppressListTileInkAssertion(); final estado = await crearEstado(); addTearDown(estado.dispose); @@ -130,7 +130,7 @@ void main() { await tester.pumpAndSettle(); 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(); 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( + 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( + find.widgetWithText(ChoiceChip, 'Flat'), + ); + expect(chip.selected, isTrue); + expect(chip.selectedColor, PluriWaveTokens.brand); + expect(chip.labelStyle?.color, const Color(0xFF062126)); + }); + testWidgets( 'Guardar como preset with a whitespace-only name shows the same validation message', (tester) async { diff --git a/test/pantallas/pantalla_ajustes_test.dart b/test/pantallas/pantalla_ajustes_test.dart index bd1239e..4ef5e4f 100644 --- a/test/pantallas/pantalla_ajustes_test.dart +++ b/test/pantallas/pantalla_ajustes_test.dart @@ -162,9 +162,10 @@ void main() { await pumpStable(tester); // 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.text('Enable equalizer'), findsOneWidget); + expect(find.byKey(const ValueKey('eq-master-switch')), findsOneWidget); }); testWidgets('tapping the Orden de listas row pushes its detail screen', ( diff --git a/test/widgets/ecualizador_widget_test.dart b/test/widgets/ecualizador_widget_test.dart index 0f20e2d..3e6b54f 100644 --- a/test/widgets/ecualizador_widget_test.dart +++ b/test/widgets/ecualizador_widget_test.dart @@ -136,4 +136,30 @@ void main() { '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(find.text('0.0dB').first); + expect(label.style?.color, PluriWaveTokens.brand.withValues(alpha: 0.9)); + expect(label.style?.color, isNot(PluriWaveTokens.dark.liveGreen)); + }); }