import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../estado/estado_radio.dart'; import '../l10n/gen/app_localizations.dart'; import 'pluri_layout.dart'; /// S1 (Tier 1 visual fidelity): extracted from `app.dart`'s old /// `_mostrarTimerDialog`, which only the single global `AppBar` could reach. /// Now that each root draws its own [PluriRootHeader] instead, this is a /// free function any of them can call directly with their own /// `BuildContext` — the sleep-timer feature stays reachable from every tab /// with no behaviour change. void showPluriSleepTimerSheet(BuildContext context) { showModalBottomSheet( context: context, // Issue 2 (feedback-pruebas): without this, the sheet is capped to a // FRACTION of the screen height. That never mattered while the sheet // always closed immediately after picking a duration (see the removed // `Navigator.pop` calls below) -- now that the countdown view actually // stays open, its title + description + headline-sized remaining-time // text can overflow that capped height on a real phone width. isScrollControlled: true, showDragHandle: true, builder: (ctx) => Consumer( builder: (ctx, estado, _) => SafeArea( child: Padding( padding: PluriLayout.sheetPadding, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocalizations.of(ctx).sleepTimer, style: Theme.of(ctx).textTheme.titleLarge, ), const SizedBox(height: PluriLayout.sectionGap), Text( AppLocalizations.of(ctx).sleepTimerDescription, style: Theme.of(ctx).textTheme.bodySmall, ), const SizedBox(height: PluriLayout.panelGap), if (estado.timer.activo) StreamBuilder( stream: estado.timer.tiempoRestanteStream, builder: (ctx, snap) { final restante = snap.data ?? estado.timer.tiempoRestante; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( _formatearDuracionTimer( AppLocalizations.of(ctx), restante, ), style: Theme.of(ctx).textTheme.headlineMedium, ), const SizedBox(height: PluriLayout.compactGap), FilledButton.tonal( onPressed: () { estado.cancelarTimer(); Navigator.pop(ctx); }, child: Text( AppLocalizations.of(ctx).cancelTimer, ), ), ], ); }, ) else Wrap( spacing: PluriLayout.compactGap, runSpacing: PluriLayout.compactGap, children: [ for (final segundos in estado.timerSuenoPresetsSegundos) ActionChip( label: Text( _formatearDuracionTimer( AppLocalizations.of(ctx), Duration(seconds: segundos), ), ), // Issue 2 (feedback-pruebas): no longer pops // the sheet -- `estado.iniciarTimerDuracion` // notifies this `Consumer`, // which swaps straight to the countdown // view above so the user actually SEES the // remaining time instead of the sheet just // closing with no feedback. onPressed: () => estado.iniciarTimerDuracion( Duration(seconds: segundos), ), ), ActionChip( avatar: const Icon(Icons.tune_rounded, size: 18), label: Text(AppLocalizations.of(ctx).optionOther), onPressed: () async { final duracion = await _pedirDuracionPersonalizada(ctx); if (duracion == null || !ctx.mounted) return; // Issue 2: same as above -- stays open on // the countdown view rather than closing. estado.iniciarTimerDuracion(duracion); }, ), ], ), ], ), ), ), ), ); } Future _pedirDuracionPersonalizada(BuildContext context) { return showModalBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, builder: (ctx) => const _TimerPersonalizadoSheet(), ); } String _formatearDuracionTimer(AppLocalizations l10n, Duration duracion) { final horas = duracion.inHours; final minutos = duracion.inMinutes.remainder(60); final segundos = duracion.inSeconds.remainder(60); if (horas > 0) { return l10n.durationHoursMinutesSeconds( horas, minutos.toString().padLeft(2, '0'), segundos.toString().padLeft(2, '0'), ); } if (minutos > 0) { return segundos == 0 ? l10n.durationMinutesOnly(minutos) : l10n.durationMinutesSeconds( minutos, segundos.toString().padLeft(2, '0'), ); } return l10n.durationSecondsOnly(segundos); } class _TimerPersonalizadoSheet extends StatefulWidget { const _TimerPersonalizadoSheet(); @override State<_TimerPersonalizadoSheet> createState() => _TimerPersonalizadoSheetState(); } class _TimerPersonalizadoSheetState extends State<_TimerPersonalizadoSheet> { final _horasCtrl = TextEditingController(); final _minutosCtrl = TextEditingController(text: '15'); final _segundosCtrl = TextEditingController(); bool _guardarPreset = true; @override void dispose() { _horasCtrl.dispose(); _minutosCtrl.dispose(); _segundosCtrl.dispose(); super.dispose(); } int _leer(TextEditingController ctrl) => int.tryParse(ctrl.text.trim()) ?? 0; Future _confirmar() async { final duracion = Duration( hours: _leer(_horasCtrl), minutes: _leer(_minutosCtrl), seconds: _leer(_segundosCtrl), ); if (duracion <= Duration.zero) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context).durationGreaterThanZero), ), ); return; } if (_guardarPreset) { await context.read().agregarTimerSuenoPreset(duracion); } if (mounted) Navigator.pop(context, duracion); } @override Widget build(BuildContext context) { final bottom = MediaQuery.viewInsetsOf(context).bottom; return SafeArea( child: Padding( padding: EdgeInsets.fromLTRB(18, 0, 18, 18 + bottom), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( AppLocalizations.of(context).customDurationTitle, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: PluriLayout.sectionGap), Row( children: [ Expanded( child: _campoTiempo( _horasCtrl, AppLocalizations.of(context).hoursLabel, ), ), const SizedBox(width: PluriLayout.compactGap), Expanded( child: _campoTiempo( _minutosCtrl, AppLocalizations.of(context).minutesLabel, ), ), const SizedBox(width: PluriLayout.compactGap), Expanded( child: _campoTiempo( _segundosCtrl, AppLocalizations.of(context).secondsLabel, ), ), ], ), const SizedBox(height: PluriLayout.compactGap), SwitchListTile.adaptive( contentPadding: EdgeInsets.zero, title: Text(AppLocalizations.of(context).saveQuickAccess), value: _guardarPreset, onChanged: (value) => setState(() => _guardarPreset = value), ), const SizedBox(height: PluriLayout.sectionGap), FilledButton.icon( icon: const Icon(Icons.bedtime_rounded), label: Text(AppLocalizations.of(context).startTimer), onPressed: _confirmar, ), ], ), ), ); } Widget _campoTiempo(TextEditingController controller, String label) { return TextField( controller: controller, keyboardType: TextInputType.number, decoration: InputDecoration( labelText: label, border: const OutlineInputBorder(), ), ); } }