fix(chrome): drop the global AppBar, give each root its own title row
The prototype (t4) draws no global app bar anywhere: every root paints a plain ~56px title row inside its own content instead (Alarmas line 325, Ajustes line 511, Explorar line 641). app.dart wrapped every tab in PluriWaveScaffold(appBar: AppBar(title: Text(appTitle), ...)), adding 56dp of chrome and a "PluriWave" title the prototype never shows. Add PluriRootHeader, a shared 56px title-row widget reused by all 5 roots. Extract app.dart's old _mostrarTimerDialog (only reachable from the removed AppBar action) into a free function, showPluriSleepTimerSheet, so every root's header can open the same sheet directly and the sleep-timer feature stays reachable from every tab with no behaviour change. S1, Tier 1 visual-fidelity pass (audit id 2521).
This commit is contained in:
-259
@@ -217,16 +217,6 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal>
|
||||
final indice = navegacion.indice;
|
||||
|
||||
return PluriWaveScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.appTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bedtime_outlined),
|
||||
tooltip: l10n.sleepTimer,
|
||||
onPressed: () => _mostrarTimerDialog(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: AnimatedSwitcher(
|
||||
@@ -423,253 +413,4 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _mostrarTimerDialog(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder:
|
||||
(ctx) => Consumer<EstadoRadio>(
|
||||
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<Duration>(
|
||||
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),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
estado.iniciarTimerDuracion(
|
||||
Duration(seconds: segundos),
|
||||
);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
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;
|
||||
estado.iniciarTimerDuracion(duracion);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Duration?> _pedirDuracionPersonalizada(BuildContext context) {
|
||||
return showModalBottomSheet<Duration>(
|
||||
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<void> _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<EstadoRadio>().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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/pluri_layout.dart';
|
||||
import '../widgets/pluri_premium_widgets.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';
|
||||
@@ -29,6 +31,14 @@ class PantallaAjustes extends StatelessWidget {
|
||||
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),
|
||||
),
|
||||
PluriScreenHeader(
|
||||
title: l10n.settingsTitle,
|
||||
subtitle: l10n.settingsSubtitle,
|
||||
|
||||
@@ -18,6 +18,8 @@ import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/pluri_layout.dart';
|
||||
import '../widgets/pluri_premium_widgets.dart';
|
||||
import '../widgets/pluri_push_scaffold.dart';
|
||||
import '../widgets/pluri_root_header.dart';
|
||||
import '../widgets/pluri_sleep_timer_sheet.dart';
|
||||
import 'pantalla_vacaciones.dart';
|
||||
|
||||
class PantallaAlarmas extends StatelessWidget {
|
||||
@@ -33,6 +35,14 @@ class PantallaAlarmas extends StatelessWidget {
|
||||
child: 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.alarmScreenTitle,
|
||||
onSleepTimer: () => showPluriSleepTimerSheet(context),
|
||||
),
|
||||
PluriScreenHeader(
|
||||
title: l10n.alarmScreenTitle,
|
||||
subtitle: l10n.alarmScreenSubtitle,
|
||||
|
||||
@@ -12,6 +12,8 @@ import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/pluri_layout.dart';
|
||||
import '../widgets/pluri_premium_widgets.dart';
|
||||
import '../widgets/pluri_push_scaffold.dart';
|
||||
import '../widgets/pluri_root_header.dart';
|
||||
import '../widgets/pluri_sleep_timer_sheet.dart';
|
||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||
|
||||
import 'pantalla_paises.dart';
|
||||
@@ -140,6 +142,14 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
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.searchScreenTitle,
|
||||
onSleepTimer: () => showPluriSleepTimerSheet(context),
|
||||
),
|
||||
PluriScreenHeader(
|
||||
title: l10n.searchScreenTitle,
|
||||
subtitle: l10n.searchScreenSubtitle,
|
||||
|
||||
@@ -10,6 +10,8 @@ import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/pluri_layout.dart';
|
||||
import '../widgets/pluri_premium_widgets.dart';
|
||||
import '../widgets/pluri_push_scaffold.dart';
|
||||
import '../widgets/pluri_root_header.dart';
|
||||
import '../widgets/pluri_sleep_timer_sheet.dart';
|
||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||
import 'ajustes/pantalla_ajustes_emisoras_personalizadas.dart';
|
||||
import 'ajustes/pantalla_ajustes_grupos_favoritos.dart';
|
||||
@@ -113,6 +115,14 @@ class _PantallaFavoritosState extends State<PantallaFavoritos> {
|
||||
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.favoritesTitle,
|
||||
onSleepTimer: () => showPluriSleepTimerSheet(context),
|
||||
),
|
||||
PluriScreenHeader(
|
||||
title: l10n.favoritesTitle,
|
||||
subtitle: l10n.favoritesHeaderSubtitle,
|
||||
@@ -180,6 +190,11 @@ class _PantallaFavoritosState extends State<PantallaFavoritos> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// S1 (Tier 1 visual fidelity): see the empty-state branch above.
|
||||
PluriRootHeader(
|
||||
title: l10n.favoritesTitle,
|
||||
onSleepTimer: () => showPluriSleepTimerSheet(context),
|
||||
),
|
||||
PluriScreenHeader(
|
||||
title: l10n.favoritesTitle,
|
||||
subtitle: l10n.favoritesHeaderSubtitle,
|
||||
|
||||
@@ -14,6 +14,8 @@ import '../tema/pluriwave_theme.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_layout.dart';
|
||||
import '../widgets/pluri_premium_widgets.dart';
|
||||
import '../widgets/pluri_root_header.dart';
|
||||
import '../widgets/pluri_sleep_timer_sheet.dart';
|
||||
import '../widgets/visualizador_audio.dart';
|
||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||
|
||||
@@ -39,6 +41,16 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
// 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).
|
||||
SliverToBoxAdapter(
|
||||
child: PluriRootHeader(
|
||||
title: l10n.navHome,
|
||||
onSleepTimer: () => showPluriSleepTimerSheet(context),
|
||||
),
|
||||
),
|
||||
// WU5 built the hero; WU6 relocated the discovery sections that
|
||||
// used to follow it (_seccionCercanas, _seccionTendencias,
|
||||
// _chipGeneros, _errorBanner, the browse grid) into
|
||||
@@ -142,7 +154,6 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// WU5, design ADR-7: the Escuchar embedded player. `EstadoRadio` is the
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../tema/pluriwave_theme.dart';
|
||||
|
||||
/// S1 (Tier 1 visual fidelity): the prototype (`t4`) draws no global
|
||||
/// `AppBar` — each root paints its own 56px title row inside its own
|
||||
/// content instead (e.g. Alarmas `height:56px;padding:0 12px 0 20px`,
|
||||
/// Ajustes `height:56px;padding:0 20px`, Explorar `height:56px`). This
|
||||
/// widget is that row, reused by all 5 roots so the sleep-timer action
|
||||
/// that used to live on `app.dart`'s single shared `AppBar` stays
|
||||
/// reachable from every tab.
|
||||
class PluriRootHeader extends StatelessWidget {
|
||||
const PluriRootHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.onSleepTimer,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final VoidCallback onSleepTimer;
|
||||
|
||||
static const double height = 56;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final type = context.pluriType;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 12, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: type.sectionTitle,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bedtime_outlined),
|
||||
tooltip: l10n.sleepTimer,
|
||||
onPressed: onSleepTimer,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
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,
|
||||
showDragHandle: true,
|
||||
builder:
|
||||
(ctx) => Consumer<EstadoRadio>(
|
||||
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<Duration>(
|
||||
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),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
estado.iniciarTimerDuracion(
|
||||
Duration(seconds: segundos),
|
||||
);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
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;
|
||||
estado.iniciarTimerDuracion(duracion);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Duration?> _pedirDuracionPersonalizada(BuildContext context) {
|
||||
return showModalBottomSheet<Duration>(
|
||||
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<void> _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<EstadoRadio>().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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user