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:
2026-07-30 16:14:11 +02:00
parent b5b9829faa
commit 533896b9fa
5 changed files with 222 additions and 29 deletions
@@ -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<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 {
@@ -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(
+75 -5
View File
@@ -113,6 +113,10 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
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<EcualizadorWidget> {
),
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<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) {
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(),