import 'package:flutter/material.dart'; import '../l10n/gen/app_localizations.dart'; import '../modelos/preset_ecualizador.dart'; import '../tema/pluriwave_theme.dart'; import '../tema/pluriwave_tokens.dart'; import 'pluri_glass_surface.dart'; class EcualizadorWidget extends StatefulWidget { final PresetEcualizador preset; final void Function(PresetEcualizador) onCambio; /// Design ADR-5: greys and disables every slider when the equalizer /// itself is off, instead of leaving fully-interactive controls that /// silently do nothing. final bool habilitado; const EcualizadorWidget({ super.key, required this.preset, required this.onCambio, this.habilitado = true, }); @override State createState() => _EcualizadorWidgetState(); } class _EcualizadorWidgetState extends State { late List _bandas; final List _etiquetas = ['60Hz', '250Hz', '1kHz', '4kHz', '16kHz']; @override void initState() { super.initState(); _bandas = List.from(widget.preset.bandas); } @override void didUpdateWidget(EcualizadorWidget old) { super.didUpdateWidget(old); if (old.preset.nombre != widget.preset.nombre) { setState(() => _bandas = List.from(widget.preset.bandas)); } } void _actualizarBanda(int index, double valor) { setState(() => _bandas[index] = valor); widget.onCambio( PresetEcualizador(nombre: 'Personalizado', bandas: List.from(_bandas)), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final tokens = context.pluriTokens; final l10n = AppLocalizations.of(context); // Design ADR-5: the title + preset Chip row that used to live here is // gone — the pushed screen's own 56px header carries the title now, and // the preset chip ROW (a different, still-reusable widget, // [PresetsEcualizadorWidget] below) is rendered by the caller alongside // this widget instead of being duplicated inside it. return PluriGlassSurface( borderRadius: BorderRadius.circular(tokens.radiusLg), padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ for (int i = 0; i < 5; i++) Expanded( child: AnimatedOpacity( duration: const Duration(milliseconds: 150), opacity: widget.habilitado ? 1.0 : 0.4, child: Card( color: tokens.listSurface.withValues(alpha: 0.6), margin: const EdgeInsets.symmetric(horizontal: 4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(tokens.radiusSm), ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 10, horizontal: 4, ), child: Column( children: [ SizedBox( // Audit 11.4: the prototype draws the band column at // 280 (t4 line 581); 152 left the sliders 46% short. height: 280, child: Semantics( slider: true, enabled: widget.habilitado, label: l10n.equalizerBandLabel(_etiquetas[i]), value: l10n.equalizerBandValue( _bandas[i].toStringAsFixed(1), ), child: RotatedBox( quarterTurns: 3, child: SliderTheme( data: SliderTheme.of(context).copyWith( trackHeight: 5, // Audit 11.6: the prototype's band fill is // brand teal (t4 line 585). `liveGreen` is // the LIVE badge colour and reading it here // put the sliders in the wrong colour family. activeTrackColor: PluriWaveTokens.brand, thumbColor: PluriWaveTokens.brand, inactiveTrackColor: 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], min: -12.0, max: 12.0, divisions: 24, onChanged: widget.habilitado ? (v) => _actualizarBanda(i, v) : null, ), ), ), ), ), 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: PluriWaveTokens.brand.withValues(alpha: 0.9), fontWeight: FontWeight.w700, ), ), Text( _etiquetas[i], style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), ], ), ), ), ), ), ], ), ); } } /// 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, 'Rock' => l10n.equalizerPresetRock, 'Pop' => l10n.equalizerPresetPop, 'Bass Boost' => l10n.equalizerPresetBassBoost, 'Jazz' => l10n.equalizerPresetJazz, 'Voz' => l10n.equalizerPresetVoice, 'Personalizado' => l10n.equalizerPresetCustom, _ => nombre, }; } class PresetsEcualizadorWidget extends StatelessWidget { final PresetEcualizador presetActual; final void Function(PresetEcualizador) onSeleccionar; /// User-saved custom presets (WU13, `eq-custom-presets` spec), appended /// after the 6 fixed ones. Additive-only parameter — defaults to empty so /// this stays the same widget, not a new one (design ADR-5: "stays /// as-is" means no restyle, not that it can never gain new data to show). /// `_nombrePreset`'s default case already falls through to the raw name, /// so a custom preset's chip label needs no special-casing here. final List personalizados; const PresetsEcualizadorWidget({ super.key, required this.presetActual, required this.onSeleccionar, this.personalizados = const [], }); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final todos = [...PresetEcualizador.presets, ...personalizados]; return Wrap( spacing: 8, runSpacing: 6, 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: 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(), ); } }