import 'dart:async'; import 'package:flutter/material.dart'; 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 '../../tema/pluriwave_tokens.dart'; import '../../widgets/ecualizador_widget.dart'; import '../../widgets/pluri_glass_surface.dart'; import '../../widgets/pluri_layout.dart'; import '../../widgets/pluri_push_scaffold.dart'; /// AUDIO group · "Ecualizador" (design ADR-3). Body moved verbatim from the /// former `_SeccionEcualizador` in `pantalla_ajustes.dart` — only the panel /// header row (icon + title + status chip) was removed, since /// [PluriPushScaffold] now carries the title and the very next row already /// shows the same active/disabled state. /// /// WU13 (design ADR-5) restyled [EcualizadorWidget] itself and added the /// 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}); @override 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 { const _CuerpoEcualizador(); @override State<_CuerpoEcualizador> createState() => _CuerpoEcualizadorState(); } class _CuerpoEcualizadorState extends State<_CuerpoEcualizador> { @override void initState() { super.initState(); final eq = context.read(); // Fire-and-forget, mirroring the established // `pantalla_ajustes_salida_audio.dart` pattern: both calls are // genuinely async (a SharedPreferences read / a native re-query), so // their completion never lands inside THIS build — no // "setState() during build" risk. unawaited(eq.cargarPresetsPersonalizados()); unawaited(eq.refrescarDispositivoActual()); } @override Widget build(BuildContext context) { // EQ state comes from EstadoEcualizador (S4-R1/S4-R5); EstadoRadio is // only consulted for the current station + favorite flag and for // resolving station names in the "ajuste propio" drill-down. return Consumer2( builder: (ctx, estado, eq, _) { final disponible = eq.disponible; final l10n = AppLocalizations.of(ctx); final emisoraActual = estado.emisoraActual; final mostrarModoPorEmisora = emisoraActual != null && estado.emisoraActualEsFavorita; final usandoEqPropio = eq.emisoraActualTienePresetPropio; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 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( 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), // 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), ], ); }, ); } } Future _abrirDialogoGuardarPreset( BuildContext context, EstadoEcualizador eq, ) { return showDialog( context: context, builder: (_) => _DialogoGuardarPreset(eq: eq), ); } /// Base-vs-per-station explainer (spec `eq-custom-presets` "Base-vs-Per- /// Station Explainer Preserved"): always visible, distinguishing the base /// (global/device) EQ this screen edits from a station's own override. class _BannerExplicacionBase extends StatelessWidget { const _BannerExplicacionBase({required this.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 Widget build(BuildContext context) { final tokens = context.pluriTokens; return Container( key: const Key('eq-base-explainer-banner'), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: tokens.liveGreen.withValues(alpha: 0.09), borderRadius: BorderRadius.circular(_bannerRadius), border: Border.all(color: tokens.liveGreen.withValues(alpha: 0.26)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.info_outline_rounded, size: 20, color: tokens.liveGreen), const SizedBox(width: 11), Expanded( child: Text( l10n.equalizerBaseExplainer, style: Theme.of(context).textTheme.bodySmall, ), ), ], ), ); } } /// "Salida activa" row (spec `eq-custom-presets` "Active Output Surfaced on /// the Main Screen"): surfaced here instead of only inside the Advanced /// (multi-device) screen, and kept live via the SAME `notifyListeners()` /// calls `_onDispositivoCambiado` already fires — no new plumbing needed /// beyond reading `dispositivoActualId` here. class _FilaSalidaActiva extends StatelessWidget { const _FilaSalidaActiva({required this.eq, required this.l10n}); final EstadoEcualizador eq; final AppLocalizations l10n; @override Widget build(BuildContext context) { final deviceId = eq.dispositivoActualId; final nombre = deviceId == null ? l10n.equalizerActiveOutputDefault : eq.nombreVisible(deviceId, eq.nombrePlataforma(deviceId)); return Padding( key: const Key('eq-active-output-row'), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Row( children: [ const Icon(Icons.speaker_group_rounded, size: 20), const SizedBox(width: 12), Expanded( child: Text( l10n.equalizerActiveOutputLabel, style: Theme.of(context).textTheme.bodyMedium, ), ), Flexible( child: Text( nombre, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.end, style: Theme.of(context).textTheme.bodySmall, ), ), ], ), ); } } /// 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, required this.estado, required this.l10n, }); final EstadoEcualizador eq; final EstadoRadio estado; final AppLocalizations l10n; @override Widget build(BuildContext context) { final uuids = eq.presetsPorEmisora.keys.toList(); 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), ), ), 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), ], ), ), ), ), ); } } /// Resolves a station uuid to its display name via [EstadoRadio.listaFavoritos] /// (the only local, synchronous list of known stations) — falls back to the /// raw uuid for a station that has its own EQ but is not (or no longer) a /// favorite, e.g. one set from the player's per-station EQ sheet (WU14). String _resolverNombreEmisora(EstadoRadio estado, String uuid) { for (final emisora in estado.listaFavoritos) { if (emisora.uuid == uuid) return emisora.nombre; } return uuid; } /// Destination screen for the drill-down row above. class _PantallaEmisorasConAjustePropio extends StatelessWidget { const _PantallaEmisorasConAjustePropio({ required this.uuids, required this.nombrePorUuid, }); final List uuids; final String Function(String uuid) nombrePorUuid; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return PluriPushScaffold( title: l10n.equalizerStationsWithOwnEqTitle, body: uuids.isEmpty ? Center(child: Text(l10n.equalizerStationsWithOwnEqEmpty)) : ListView.separated( padding: PluriLayout.pageContentPadding, itemCount: uuids.length, separatorBuilder: (_, __) => const SizedBox(height: 4), itemBuilder: (ctx, i) { final uuid = uuids[i]; return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ const Icon(Icons.radio_rounded, size: 20), const SizedBox(width: 12), Expanded(child: Text(nombrePorUuid(uuid))), ], ), ); }, ), ); } } /// "Guardar como preset" dialog (spec `eq-custom-presets` "Custom Preset /// Save" / "Custom Preset Naming Validates Non-Empty Input"). class _DialogoGuardarPreset extends StatefulWidget { const _DialogoGuardarPreset({required this.eq}); final EstadoEcualizador eq; @override State<_DialogoGuardarPreset> createState() => _DialogoGuardarPresetState(); } class _DialogoGuardarPresetState extends State<_DialogoGuardarPreset> { late final TextEditingController _nombreCtrl; String? _error; @override void initState() { super.initState(); _nombreCtrl = TextEditingController(); } @override void dispose() { _nombreCtrl.dispose(); super.dispose(); } Future _confirmar() async { final guardado = await widget.eq.guardarPresetPersonalizado( _nombreCtrl.text, ); if (!mounted) return; if (!guardado) { setState( () => _error = AppLocalizations.of(context).equalizerSavePresetEmptyNameError, ); return; } Navigator.of(context).pop(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return AlertDialog( title: Text(l10n.equalizerSavePresetDialogTitle), content: TextField( key: const Key('eq-save-preset-name-field'), controller: _nombreCtrl, autofocus: true, decoration: InputDecoration( labelText: l10n.equalizerSavePresetNameLabel, errorText: _error, ), ), actions: [ FilledButton( key: const Key('eq-save-preset-confirm-button'), onPressed: _confirmar, child: Text(l10n.equalizerSavePresetConfirm), ), ], ); } }