import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../estado/estado_ecualizador.dart'; import '../estado/estado_entitlement.dart'; import '../estado/estado_grabacion.dart'; import '../estado/estado_idioma.dart'; import '../estado/estado_radio.dart'; import '../l10n/display_names.dart'; import '../l10n/gen/app_localizations.dart'; import '../modelos/archivo_grabacion.dart'; import '../modelos/emisora.dart'; import '../tema/pluriwave_tokens.dart'; import '../widgets/hoja_premium.dart'; import '../widgets/pluri_layout.dart'; import '../widgets/pluri_push_scaffold.dart'; import '../widgets/pluri_root_header.dart'; import '../widgets/pluri_sleep_timer_sheet.dart'; import 'ajustes/pantalla_ajustes_backup.dart'; import 'ajustes/pantalla_ajustes_ecualizador.dart'; import 'ajustes/pantalla_ajustes_emisora_preferida.dart'; import 'ajustes/pantalla_ajustes_emisoras_personalizadas.dart'; import 'ajustes/pantalla_ajustes_grupos_favoritos.dart'; import 'ajustes/pantalla_ajustes_idioma.dart'; import 'ajustes/pantalla_ajustes_info.dart'; import 'ajustes/pantalla_ajustes_musica_local.dart'; import 'ajustes/pantalla_ajustes_orden_listas.dart'; import 'ajustes/pantalla_ajustes_salida_audio.dart'; import 'ajustes/pantalla_ajustes_timer_sueno.dart'; import 'ajustes/widgets/fila_ajuste.dart'; import 'pantalla_grabaciones.dart'; class PantallaAjustes extends StatelessWidget { const PantallaAjustes({super.key}); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return ListView( padding: PluriLayout.pageListPadding, children: [ // S1 (Tier 1 visual fidelity): the prototype has no global AppBar — // this root now draws its own 56px title row instead of relying on // app.dart's removed shared chrome (which is also where the // sleep-timer action used to live). PluriRootHeader( title: l10n.settingsTitle, onSleepTimer: () => showPluriSleepTimerSheet(context), ), const Padding( padding: PluriLayout.pageContentPadding, child: _AjustesContent(), ), ], ); } } /// Design ADR-3: the Settings root is grouped nav rows only — each /// [FilaAjuste] pushes its own detail screen via `PluriPushScaffold.push`, /// carrying zero inline controls. All 12 sections are now decomposed across /// WU3a (AUDIO, EMISORAS) and WU3b (GRABACIONES Y MÚSICA, APLICACIÓN); sleep /// timer and backup/restore stay reachable throughout — nothing was dropped. class _AjustesContent extends StatelessWidget { const _AjustesContent(); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); // S8 (Tier 1 visual fidelity): the prototype shows every row's current // value (t4 lines 512-539, 625). 8 of the 12 rows below read it from // state already provided at the app root — no new providers needed. // // S4-R5 convention (see pantalla_inicio.dart, pantalla_buscar.dart): // `context.select` per scalar, NOT a root `context.watch()` // — EstadoRadio also notifies on audio buffer/position events, which // this screen has nothing to do with. A root watch here rebuilds the // WHOLE settings list (including the Grabaciones row's FutureBuilder, // which would re-issue `listarGrabaciones()` on every single one of // those unrelated notifications) far more often than intended. final gruposCount = context.select( (e) => e.gruposFavoritos.length, ); final emisoraPreferida = context.select( (e) => e.emisoraPreferida, ); final emisorasCustomCount = context.select( (e) => e.emisorasCustom.length, ); final ordenListas = context.select( (e) => e.ordenListas, ); final timerActivo = context.select( (e) => e.timer.activo, ); final ecualizadorActivo = context.select( (e) => e.activo, ); final grabacion = context.watch(); final idioma = context.select( (e) => e.localeSeleccionado, ); final esPremium = context.select( (e) => e.esPremium, ); return Column( children: [ GrupoAjustes( titulo: l10n.settingsGroupAudioTitle, filas: [ FilaAjuste( icon: Icons.equalizer_rounded, // Audit 10.5 (t4 line 514): the first AUDIO row's icon is // brand cyan. iconColor: PluriWaveTokens.brand, titulo: l10n.equalizerTitle, valor: ecualizadorActivo ? l10n.equalizerActive : l10n.equalizerDisabled, onTap: () => PluriPushScaffold.push( context, (_) => const PantallaAjustesEcualizador(), ), ), // No `valor`: per-device output naming needs a friendly-name // lookup this root doesn't have (only a raw device id). FilaAjuste( icon: Icons.devices_rounded, titulo: l10n.advancedEqSectionTitle, onTap: () => PluriPushScaffold.push( context, (_) => const PantallaAjustesSalidaAudio(), ), ), FilaAjuste( icon: Icons.bedtime_rounded, titulo: l10n.timerSectionTitle, // Reuses the equalizer's own "Active" string (generic enough // in every locale) — shown only while running, matching how // the preferred-station row shows nothing when unset. valor: timerActivo ? l10n.equalizerActive : null, onTap: () => PluriPushScaffold.push( context, (_) => const PantallaAjustesTimerSueno(), ), ), ], ), // Issue 3 (feedback-pruebas): t4:523/534/541 all draw a 16px gap // between these opaque, stacked settings groups -- not 12. const SizedBox(height: 16, key: ValueKey('ajustes-group-gap-1')), GrupoAjustes( titulo: l10n.settingsGroupStationsTitle, filas: [ FilaAjuste( icon: Icons.playlist_add_check_circle_rounded, // Audit 10.5 (t4 line 526): the first STATIONS row's icon // is warmCoral (the prototype's own `folder` row accent). iconColor: PluriWaveTokens.dark.warmCoral, titulo: l10n.favoriteGroupsTitle, valor: '$gruposCount', onTap: () => PluriPushScaffold.push( context, (_) => const PantallaAjustesGruposFavoritos(), ), ), FilaAjuste( icon: Icons.radio_rounded, titulo: l10n.preferredStationTitle, valor: emisoraPreferida != null ? localizedStationName(l10n, emisoraPreferida.nombre) : l10n.dash, onTap: () => PluriPushScaffold.push( context, (_) => const PantallaAjustesEmisoraPreferida(), ), ), FilaAjuste( icon: Icons.add_circle_outline_rounded, titulo: l10n.customStationsTitle, valor: '$emisorasCustomCount', onTap: () => PluriPushScaffold.push( context, (_) => const PantallaAjustesEmisorasPersonalizadas(), ), ), FilaAjuste( icon: Icons.sort_rounded, titulo: l10n.stationOrderTitle, valor: ordenListas == OrdenEmisoras.calidad ? l10n.stationOrderByQuality : l10n.stationOrderByName, onTap: () => PluriPushScaffold.push( context, (_) => const PantallaAjustesOrdenListas(), ), ), ], ), const SizedBox(height: 16, key: ValueKey('ajustes-group-gap-2')), GrupoAjustes( titulo: l10n.settingsGroupRecordingsTitle, filas: [ // FutureBuilder-wrapped (not a plain FilaAjuste): the count · // size value needs an async disk listing // (EstadoGrabacion.listarGrabaciones), same source // PantallaGrabaciones itself reads. FutureBuilder>( future: grabacion.listarGrabaciones(), builder: (context, snapshot) { final archivos = snapshot.data; return FilaAjuste( icon: Icons.radio_button_checked_rounded, titulo: l10n.recordingsSectionTitle, valor: archivos == null ? null : '${archivos.length} · ${_totalMb(archivos)} MB', // WU15b: this row opens the recordings LIBRARY // (PantallaGrabaciones), matching the approved mockup's // "Ajustes > Grabaciones" screen. The folder/size // settings form (PantallaAjustesGrabaciones) is still // reachable, but now from within the library via its own // settings action. onTap: () => PluriPushScaffold.push( context, (_) => const PantallaGrabaciones(), ), ); }, ), // No `valor`: the configured folder is a SAF tree URI resolved // by an async native channel call // (FuenteMusicaLocalAutoImpl.carpetaActual) this root would // need to invoke itself, unmocked, just to render a value. FilaAjuste( icon: Icons.library_music_outlined, titulo: l10n.localMusicSectionTitle, onTap: () => PluriPushScaffold.push( context, (_) => const PantallaAjustesMusicaLocal(), ), ), ], ), const SizedBox(height: 16, key: ValueKey('ajustes-group-gap-3')), GrupoAjustes( titulo: l10n.settingsGroupApplicationTitle, filas: [ // freemium-gating spec "Settings always shows a premium row": // a persistent buy row (free tier) or a premium-active state // with restore access (premium tier) — both open the same // paywall sheet, which adapts its own body to the tier. FilaAjuste( key: const ValueKey('ajustes-fila-premium'), icon: Icons.workspace_premium_rounded, iconColor: PluriWaveTokens.brand, titulo: l10n.funcionPremium, valor: esPremium ? l10n.equalizerActive : null, onTap: () => mostrarHojaPremium(context), ), FilaAjuste( icon: Icons.language_rounded, titulo: l10n.languageSectionTitle, valor: nombreIdiomaActual(idioma, l10n), onTap: () => PluriPushScaffold.push( context, (_) => const PantallaAjustesIdioma(), ), ), // No `valor`: there is no persisted "last backup" timestamp to // read — showing one here would mean fabricating it. FilaAjuste( icon: Icons.backup_outlined, titulo: l10n.backupSectionTitle, onTap: () => PluriPushScaffold.push( context, (_) => const PantallaAjustesBackup(), ), ), // No `valor`: PackageInfo.fromPlatform() has no test-environment // fallback anywhere else in this codebase either (see // PantallaAjustesInfo's own FutureBuilder) — wiring it here // would add a value this pass cannot deterministically test. FilaAjuste( icon: Icons.info_outline_rounded, titulo: l10n.infoSectionTitle, onTap: () => PluriPushScaffold.push( context, (_) => const PantallaAjustesInfo(), ), ), ], ), ], ); } static int _totalMb(List archivos) { final totalBytes = archivos.fold(0, (a, b) => a + b.tamanoBytes); return (totalBytes / (1024 * 1024)).round(); } }