fix(eq): split the equalizer body into 4 cards, add Restablecer a plano

Audit 11.10/11.8 (t4 lines 570-602): the prototype draws the explainer
banner, the band sliders, "Salida activa + Guardar como preset +
Restablecer a plano" and "Emisoras con ajuste propio" as 4 separate
cards. The build nested everything in one outer PluriGlassSurface, so
it read as a single merged block, and had no reset-to-flat action at
all.

Removes the outer wrapper from _CuerpoEcualizador. EcualizadorWidget
already draws its own card (design ADR-5, unchanged); the explainer
banner already has its own tinted background (unchanged); "Emisoras
con ajuste propio" now wraps itself in its own PluriGlassSurface; a
new _TarjetaSalidaYAcciones groups Salida activa with two tappable
rows — "Guardar como preset" (relocated from a floating end-aligned
button) and the new "Restablecer a plano", which applies
PresetEcualizador.flat via the existing EstadoEcualizador.cambiarPreset
— the same call the "Plano" preset chip already makes, not a new
capability.

The 5-slider band count, servicio_ecualizador.dart and
preset_ecualizador.dart are untouched.
This commit is contained in:
2026-07-30 00:57:29 +02:00
parent e5d461af53
commit 653848899f
29 changed files with 373 additions and 96 deletions
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import '../../estado/estado_ecualizador.dart';
import '../../estado/estado_radio.dart';
import '../../l10n/gen/app_localizations.dart';
import '../../modelos/preset_ecualizador.dart';
import '../../tema/pluriwave_theme.dart';
import '../../widgets/ecualizador_widget.dart';
import '../../widgets/pluri_glass_surface.dart';
@@ -22,6 +23,15 @@ import '../../widgets/pluri_push_scaffold.dart';
/// base-vs-per-station explainer, the "Salida activa" summary row, the
/// "Emisoras con ajuste propio" drill-down, and the "Guardar como preset"
/// custom-preset flow — see `_CuerpoEcualizador` below.
///
/// Audit 11.10/11.8 (t4 lines 570-602, item 19): the prototype draws 4
/// SEPARATE cards (explainer banner / band sliders / "Salida activa +
/// Guardar como preset + Restablecer a plano" / "Emisoras con ajuste
/// propio"), not one shared glass surface wrapping everything — and adds
/// a "Restablecer a plano" action the build never had. `_CuerpoEcualizador`
/// no longer wraps its children in one outer `PluriGlassSurface`; each
/// section now either already draws its own background
/// ([EcualizadorWidget], unchanged) or gained one here.
class PantallaAjustesEcualizador extends StatelessWidget {
const PantallaAjustesEcualizador({super.key});
@@ -70,67 +80,66 @@ class _CuerpoEcualizadorState extends State<_CuerpoEcualizador> {
emisoraActual != null && estado.emisoraActualEsFavorita;
final usandoEqPropio = eq.emisoraActualTienePresetPropio;
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
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,
),
if (mostrarModoPorEmisora) ...[
const SizedBox(height: 8),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: Text(l10n.equalizerEnable),
title: Text(l10n.equalizerPerStationTitle),
subtitle: Text(
disponible
? l10n.equalizerRealtimeSubtitle
: l10n.equalizerPendingSubtitle,
),
value: eq.activo,
onChanged: eq.cambiarActivo,
),
if (mostrarModoPorEmisora) ...[
const SizedBox(height: 8),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: Text(l10n.equalizerPerStationTitle),
subtitle: Text(
usandoEqPropio
? l10n.equalizerPerStationActive(emisoraActual.nombre)
: l10n.equalizerPerStationMain(emisoraActual.nombre),
),
value: usandoEqPropio,
onChanged:
(usarPropio) =>
eq.cambiarModoEmisoraActual(usarPropio: usarPropio),
),
],
const SizedBox(height: 12),
_BannerExplicacionBase(l10n: l10n),
const SizedBox(height: 8),
_FilaSalidaActiva(eq: eq, l10n: l10n),
const SizedBox(height: 4),
_FilaEmisorasConAjustePropio(eq: eq, estado: estado, l10n: l10n),
const SizedBox(height: 12),
PresetsEcualizadorWidget(
presetActual: eq.presetActual,
personalizados: eq.presetsPersonalizados,
onSeleccionar: (p) => eq.cambiarPreset(p),
),
const SizedBox(height: 12),
EcualizadorWidget(
preset: eq.presetActual,
habilitado: eq.activo,
onCambio: (p) => eq.cambiarPreset(p),
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
key: const Key('eq-save-preset-action'),
onPressed: () => _abrirDialogoGuardarPreset(context, eq),
icon: const Icon(Icons.bookmark_add_outlined, size: 20),
label: Text(l10n.equalizerSaveAsPresetAction),
usandoEqPropio
? l10n.equalizerPerStationActive(emisoraActual.nombre)
: l10n.equalizerPerStationMain(emisoraActual.nombre),
),
value: usandoEqPropio,
onChanged:
(usarPropio) =>
eq.cambiarModoEmisoraActual(usarPropio: usarPropio),
),
],
),
const SizedBox(height: 12),
// CARD 1 (t4 line 571) — already its own tinted Container.
_BannerExplicacionBase(l10n: l10n),
const SizedBox(height: 16),
// Bare row, no card (t4 lines 573-578) — unchanged.
PresetsEcualizadorWidget(
presetActual: eq.presetActual,
personalizados: eq.presetsPersonalizados,
onSeleccionar: (p) => eq.cambiarPreset(p),
),
const SizedBox(height: 14),
// CARD 2 (t4 lines 580-590) — EcualizadorWidget already draws
// its own PluriGlassSurface (design ADR-5), unchanged here.
EcualizadorWidget(
preset: eq.presetActual,
habilitado: eq.activo,
onCambio: (p) => eq.cambiarPreset(p),
),
const SizedBox(height: 14),
// CARD 3 (t4 lines 592-598): Salida activa / Guardar como
// preset / Restablecer a plano, grouped.
_TarjetaSalidaYAcciones(
eq: eq,
l10n: l10n,
onGuardarPreset: () => _abrirDialogoGuardarPreset(context, eq),
),
const SizedBox(height: 14),
// CARD 4 (t4 lines 600-602) — its own PluriGlassSurface now.
_FilaEmisorasConAjustePropio(eq: eq, estado: estado, l10n: l10n),
],
);
},
);
@@ -204,7 +213,7 @@ class _FilaSalidaActiva extends StatelessWidget {
return Padding(
key: const Key('eq-active-output-row'),
padding: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
child: Row(
children: [
const Icon(Icons.speaker_group_rounded, size: 20),
@@ -230,9 +239,107 @@ class _FilaSalidaActiva extends StatelessWidget {
}
}
/// Audit 11.8 (t4 lines 592-598): groups "Salida activa", "Guardar como
/// preset" and the entirely-missing "Restablecer a plano" into ONE card —
/// the build previously drew the first as a bare row and the second as a
/// floating, right-aligned button at the bottom of the whole screen.
/// "Restablecer a plano" is not a new capability: `PresetEcualizador.flat`
/// is the SAME preset the "Plano" chip in [PresetsEcualizadorWidget]
/// already applies via `EstadoEcualizador.cambiarPreset` — this is just a
/// second, prototype-mandated entry point to it.
class _TarjetaSalidaYAcciones extends StatelessWidget {
const _TarjetaSalidaYAcciones({
required this.eq,
required this.l10n,
required this.onGuardarPreset,
});
final EstadoEcualizador eq;
final AppLocalizations l10n;
final VoidCallback onGuardarPreset;
@override
Widget build(BuildContext context) {
return PluriGlassSurface(
key: const Key('eq-card-output-actions'),
borderRadius: BorderRadius.circular(context.pluriTokens.radiusMd),
padding: EdgeInsets.zero,
child: Column(
children: [
_FilaSalidaActiva(eq: eq, l10n: l10n),
const Divider(height: 1),
_FilaAccionTarjeta(
key: const Key('eq-save-preset-action'),
icon: Icons.bookmark_add_outlined,
label: l10n.equalizerSaveAsPresetAction,
trailing: const Icon(Icons.chevron_right_rounded, size: 19),
onTap: onGuardarPreset,
),
const Divider(height: 1),
_FilaAccionTarjeta(
key: const Key('eq-reset-flat-action'),
icon: Icons.restart_alt_rounded,
label: l10n.equalizerResetToFlatAction,
onTap: () => eq.cambiarPreset(PresetEcualizador.flat),
),
],
),
);
}
}
/// One tappable icon+label row inside [_TarjetaSalidaYAcciones] — shared
/// shape for "Guardar como preset" and "Restablecer a plano" (t4 lines
/// 595, 597: identical row layout, only "Guardar..." has a trailing
/// chevron since it opens a dialog rather than acting immediately).
class _FilaAccionTarjeta extends StatelessWidget {
const _FilaAccionTarjeta({
super.key,
required this.icon,
required this.label,
required this.onTap,
this.trailing,
});
final IconData icon;
final String label;
final VoidCallback onTap;
final Widget? trailing;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
child: Row(
children: [
Icon(icon, size: 21),
const SizedBox(width: 12),
Expanded(
child: Text(
label,
style: Theme.of(context).textTheme.bodyMedium,
),
),
if (trailing != null) trailing!,
],
),
),
),
);
}
}
/// "Emisoras con ajuste propio" drill-down row (spec `eq-custom-presets`
/// "Stations-With-Own-EQ Drill-Down"): sourced from the existing
/// `presetsPorEmisora` map, no new state.
///
/// Audit 11.10 (t4 lines 600-602): CARD 4 — now wraps itself in its own
/// `PluriGlassSurface` instead of being one more row inside a shared
/// glass block.
class _FilaEmisorasConAjustePropio extends StatelessWidget {
const _FilaEmisorasConAjustePropio({
required this.eq,
@@ -247,48 +354,53 @@ class _FilaEmisorasConAjustePropio extends StatelessWidget {
@override
Widget build(BuildContext context) {
final uuids = eq.presetsPorEmisora.keys.toList();
return Material(
type: MaterialType.transparency,
child: InkWell(
key: const Key('eq-stations-own-eq-row'),
borderRadius: BorderRadius.circular(context.pluriTokens.radiusSm),
onTap:
() => PluriPushScaffold.push(
context,
(_) => _PantallaEmisorasConAjustePropio(
uuids: uuids,
nombrePorUuid: (uuid) => _resolverNombreEmisora(estado, uuid),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
const Icon(Icons.tune_rounded, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.equalizerStationsWithOwnEqTitle,
style: Theme.of(context).textTheme.bodyMedium,
),
Text(
l10n.equalizerStationsWithOwnEqSubtitle,
style: Theme.of(context).textTheme.bodySmall,
),
],
return PluriGlassSurface(
key: const Key('eq-card-stations-own-eq'),
borderRadius: BorderRadius.circular(context.pluriTokens.radiusMd),
padding: EdgeInsets.zero,
child: Material(
type: MaterialType.transparency,
child: InkWell(
key: const Key('eq-stations-own-eq-row'),
borderRadius: BorderRadius.circular(context.pluriTokens.radiusMd),
onTap:
() => PluriPushScaffold.push(
context,
(_) => _PantallaEmisorasConAjustePropio(
uuids: uuids,
nombrePorUuid: (uuid) => _resolverNombreEmisora(estado, uuid),
),
),
Text(
key: const Key('eq-stations-own-eq-count'),
'${uuids.length}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(width: 4),
const Icon(Icons.chevron_right_rounded, size: 20),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
child: Row(
children: [
const Icon(Icons.tune_rounded, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.equalizerStationsWithOwnEqTitle,
style: Theme.of(context).textTheme.bodyMedium,
),
Text(
l10n.equalizerStationsWithOwnEqSubtitle,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
Text(
key: const Key('eq-stations-own-eq-count'),
'${uuids.length}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(width: 4),
const Icon(Icons.chevron_right_rounded, size: 20),
],
),
),
),
),