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 '../../tema/pluriwave_theme.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. 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()], ), ); } 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 PluriGlassSurface( child: 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.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), ), ), ], ), ); }, ); } } 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; @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(tokens.radiusSm), 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(vertical: 4), 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, ), ), ], ), ); } } /// "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. 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 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, ), ], ), ), 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), ), ], ); } }