import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../estado/estado_alarmas.dart'; import '../estado/estado_radio.dart'; import '../l10n/display_names.dart'; import '../l10n/app_localizations_ext.dart'; import '../l10n/gen/app_localizations.dart'; import '../modelos/alarma_musical.dart'; import '../modelos/emisora.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_icon.dart'; import '../widgets/pluri_layout.dart'; import '../widgets/pluri_premium_widgets.dart'; class PantallaAlarmas extends StatelessWidget { const PantallaAlarmas({super.key}); @override Widget build(BuildContext context) { final estado = context.watch(); final l10n = AppLocalizations.of(context); return RefreshIndicator( onRefresh: estado.refrescarProgramacion, child: ListView( padding: PluriLayout.pageListPadding, children: [ PluriScreenHeader( title: l10n.alarmScreenTitle, subtitle: l10n.alarmScreenSubtitle, glyph: PluriIconGlyph.alarm, primaryActionLabel: l10n.createAlarmAction, onPrimaryAction: () => _abrirEditor(context), trailing: PluriStatusPill( icon: Icons.alarm_on_rounded, label: l10n.alarmsCount(estado.alarmas.length), ), ), Padding( padding: PluriLayout.pageContentPadding, child: Column( children: [ _PanelProximaAlarma(estado: estado), const SizedBox(height: 12), if (estado.alarmas.isEmpty) const _EmptyAlarmas() else for (final alarma in estado.alarmas) ...[ _TarjetaAlarma(alarma: alarma), const SizedBox(height: 12), ], _PanelVacaciones(estado: estado), const SizedBox(height: 12), _AccesoDiagnostico(estado: estado), ], ), ), ], ), ); } Future _abrirEditor( BuildContext context, { AlarmaMusical? alarma, }) async { await showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, builder: (_) => _EditorAlarmaSheet(alarma: alarma), ); } } class _PanelProximaAlarma extends StatelessWidget { const _PanelProximaAlarma({required this.estado}); final EstadoAlarmas estado; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final proxima = estado.proximaAlarma; final activasSinProxima = estado.alarmas .where((a) => a.activa && a.proximaProgramable == null) .length; final proximaProgramable = proxima?.proximaProgramable; return PluriGlassSurface( glowColor: const Color(0xFFFFB86B).withValues(alpha: 0.28), child: Row( children: [ const _AssetIcon('assets/icons/alarmas/alarm_music.png', size: 72), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( proxima == null ? activasSinProxima > 0 ? l10n.activeAlarmsWithoutNextTitle : l10n.noActiveAlarms : l10n.nextAlarmTitle, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, ), ), const SizedBox(height: 4), Text( proxima == null ? activasSinProxima > 0 ? l10n.activeAlarmsWithoutNextSubtitle( activasSinProxima, ) : l10n.createAlarmHint : '${_nombreVisibleAlarma(l10n, proxima)} · ${_fechaHora(l10n, proximaProgramable!)}', ), ], ), ), ], ), ); } } class _TarjetaAlarma extends StatelessWidget { const _TarjetaAlarma({required this.alarma}); final AlarmaMusical alarma; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final estado = context.watch(); final excepcion = estado.ultimaExcepcionPara(alarma.id); final mensajeVacaciones = _mensajeVacaciones(l10n, estado.vacaciones); return PluriGlassSurface( glowColor: const Color(0xFF22D3EE).withValues(alpha: 0.22), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const _AssetIcon( 'assets/icons/alarmas/alarm_music.png', size: 64, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _hora(alarma), style: Theme.of( context, ).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.w900, letterSpacing: -1, ), ), Text(_nombreVisibleAlarma(l10n, alarma)), ], ), ), Switch.adaptive( value: alarma.activa, onChanged: (value) => estado.cambiarActiva(alarma, value), ), ], ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ _InfoChip( icon: Icons.repeat_rounded, label: _programacion(l10n, alarma), ), _InfoChip( icon: Icons.beach_access_rounded, label: alarma.sonarEnVacaciones ? l10n.alarmVacationPlay : l10n.alarmVacationPause, ), _InfoChip( icon: Icons.volume_up_rounded, label: '${(alarma.volumen * 100).round()}%', ), _InfoChip( icon: Icons.trending_up_rounded, label: l10n.alarmFadeInLabel(alarma.fadeInSegundos), ), ], ), const SizedBox(height: 12), if (alarma.proximaProgramable != null) _NoticeLine( icon: Icons.event_available_rounded, text: l10n.alarmNextExecution( _fechaHora(l10n, alarma.proximaProgramable!), ), ) else _NoticeLine( icon: Icons.pause_circle_outline_rounded, text: l10n.alarmNoNextExecution, ), if (excepcion != null) ...[ const SizedBox(height: 6), _NoticeLine( icon: Icons.skip_next_rounded, text: l10n.alarmSkippedExecution( _fechaHora(l10n, excepcion.ejecucion), ), ), ], if (mensajeVacaciones != null) ...[ const SizedBox(height: 6), _NoticeLine( icon: Icons.beach_access_rounded, text: mensajeVacaciones, ), ], const SizedBox(height: 12), Row( children: [ TextButton.icon( icon: const Icon(Icons.edit_rounded), label: Text(l10n.editAction), onPressed: () => _abrirEditor(context, alarma: alarma), ), TextButton.icon( icon: const Icon(Icons.skip_next_rounded), label: Text(l10n.skipNextAction), onPressed: alarma.proximaProgramable == null ? null : () async { await estado.saltarProxima(alarma.id); if (context.mounted) { final alarmas = context.read().alarmas; AlarmaMusical? actualizada; for (final item in alarmas) { if (item.id == alarma.id) { actualizada = item; break; } } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( actualizada?.proximaProgramable == null ? l10n.alarmSkippedNoNextSnackbar : l10n.alarmSkippedReturnsSnackbar( _fechaHora( l10n, actualizada!.proximaProgramable!, ), ), ), ), ); } }, ), const Spacer(), IconButton( tooltip: l10n.deleteAction, icon: const Icon(Icons.delete_outline_rounded), onPressed: () => estado.eliminarAlarma(alarma.id), ), ], ), ], ), ); } String? _mensajeVacaciones( AppLocalizations l10n, List vacaciones, ) { if (alarma.sonarEnVacaciones) return null; final ahora = DateTime.now(); RangoVacaciones? actual; for (final rango in vacaciones) { if (rango.contiene(ahora)) { actual = rango; break; } } if (actual != null) { if (alarma.proximaProgramable == null) { return l10n.alarmVacationPausedNoNext( _nombreVisibleVacaciones(l10n, actual), ); } return l10n.alarmVacationPausedReturns( _nombreVisibleVacaciones(l10n, actual), _fechaHora(l10n, alarma.proximaProgramable!), ); } if (alarma.proximaProgramable != null) { return l10n.alarmVacationReturns( _fechaHora(l10n, alarma.proximaProgramable!), ); } return null; } void _abrirEditor(BuildContext context, {required AlarmaMusical alarma}) { showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, builder: (_) => _EditorAlarmaSheet(alarma: alarma), ); } } class _EditorAlarmaSheet extends StatefulWidget { const _EditorAlarmaSheet({this.alarma}); final AlarmaMusical? alarma; @override State<_EditorAlarmaSheet> createState() => _EditorAlarmaSheetState(); } class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> { late final TextEditingController _nombreController; late TimeOfDay _hora; late DateTime _fecha; late TipoProgramacionAlarma _tipo; late Set _diasSemana; late double _volumen; late int _fadeInSegundos; late bool _sonarEnVacaciones; late SonidoInternoAlarma _sonidoInterno; Emisora? _emisora; bool _favoritosSolicitados = false; @override void initState() { super.initState(); final alarma = widget.alarma; final l10n = AppLocalizations.of(context); final ahora = DateTime.now().add(const Duration(minutes: 5)); _nombreController = TextEditingController( text: alarma == null ? l10n.defaultAlarmName : _nombreVisibleAlarma(l10n, alarma), ); _hora = TimeOfDay( hour: alarma?.hora ?? ahora.hour, minute: alarma?.minuto ?? ahora.minute, ); _fecha = alarma?.fechaUnica ?? ahora; _tipo = alarma?.tipoProgramacion ?? TipoProgramacionAlarma.unica; _diasSemana = {...alarma?.diasSemana ?? const []}; _volumen = alarma?.volumen ?? 0.85; _fadeInSegundos = (alarma?.fadeInSegundos ?? 0).clamp(0, 60).toInt(); _sonarEnVacaciones = alarma?.sonarEnVacaciones ?? true; _sonidoInterno = alarma?.sonidoInterno ?? SonidoInternoAlarma.amanecer; _emisora = alarma?.emisora ?? context.read().emisoraPreferida; } @override void dispose() { _nombreController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final radio = context.watch(); final bottom = MediaQuery.of(context).viewInsets.bottom; if (!_favoritosSolicitados) { _favoritosSolicitados = true; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) context.read().cargarFavoritos(); }); } if (_emisora == null && widget.alarma == null && radio.emisoraPreferida != null) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && _emisora == null) { setState(() => _emisora = radio.emisoraPreferida); } }); } final favoritas = _favoritasConSeleccion(radio.listaFavoritos); return Padding( padding: EdgeInsets.fromLTRB(12, 12, 12, bottom + 12), child: PluriGlassSurface( borderRadius: BorderRadius.circular(28), padding: const EdgeInsets.all(18), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const _AssetIcon( 'assets/icons/alarmas/alarm_music.png', size: 58, ), const SizedBox(width: 12), Expanded( child: Text( widget.alarma == null ? l10n.newAlarmTitle : l10n.editAlarmTitle, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w900, ), ), ), IconButton( icon: const Icon(Icons.close_rounded), onPressed: () => Navigator.pop(context), ), ], ), const SizedBox(height: 14), TextField( controller: _nombreController, decoration: InputDecoration(labelText: l10n.nameLabel), ), const SizedBox(height: 12), Row( children: [ Expanded( child: _PickerButton( icon: Icons.schedule_rounded, label: l10n.timeField, value: _hora.format(context), onTap: _elegirHora, ), ), const SizedBox(width: 10), Expanded( child: _PickerButton( icon: Icons.event_rounded, label: l10n.dateField, value: _fechaCorta(_fecha), onTap: _tipo == TipoProgramacionAlarma.unica ? _elegirFecha : null, ), ), ], ), const SizedBox(height: 12), SegmentedButton( segments: [ ButtonSegment( value: TipoProgramacionAlarma.unica, label: Text(l10n.oneTimeOption), ), ButtonSegment( value: TipoProgramacionAlarma.diaria, label: Text(l10n.dailyOption), ), ButtonSegment( value: TipoProgramacionAlarma.diasSemana, label: Text(l10n.weekdaysOption), ), ], selected: {_tipo}, onSelectionChanged: (value) => setState(() => _tipo = value.first), ), if (_tipo == TipoProgramacionAlarma.diasSemana) ...[ const SizedBox(height: 10), Wrap( spacing: 6, children: [ for (var i = DateTime.monday; i <= DateTime.sunday; i++) FilterChip( label: Text(_weekdayShort(l10n, i)), selected: _diasSemana.contains(i), onSelected: (selected) => setState(() { selected ? _diasSemana.add(i) : _diasSemana.remove(i); }), ), ], ), ], const SizedBox(height: 14), _SectionLabel( icon: 'assets/icons/alarmas/fallback_sound.png', text: l10n.soundAndVolumeSection, ), Slider( value: _volumen, min: 0.25, max: 1, divisions: 15, label: '${(_volumen * 100).round()}%', onChanged: (value) => setState(() => _volumen = value), ), const SizedBox(height: 8), ListTile( contentPadding: EdgeInsets.zero, title: Text(l10n.alarmFadeInTitle), subtitle: Text( _fadeInSegundos == 0 ? l10n.alarmFadeInOff : l10n.alarmFadeInSummary(_fadeInSegundos), ), ), Slider( value: _fadeInSegundos.toDouble(), min: 0, max: 60, divisions: 60, label: '${_fadeInSegundos}s', onChanged: (value) => setState(() => _fadeInSegundos = value.round()), ), DropdownButtonFormField( initialValue: _sonidoInterno, decoration: InputDecoration( labelText: l10n.internalSafeSoundLabel, ), items: [ DropdownMenuItem( value: SonidoInternoAlarma.amanecer, child: Text(l10n.soundWarmSunrise), ), DropdownMenuItem( value: SonidoInternoAlarma.campanaSuave, child: Text(l10n.soundSoftBell), ), DropdownMenuItem( value: SonidoInternoAlarma.pulsoDigital, child: Text(l10n.soundDigitalPulse), ), ], onChanged: (value) => setState( () => _sonidoInterno = value ?? _sonidoInterno, ), ), const SizedBox(height: 8), DropdownButtonFormField( key: ValueKey(_emisora?.uuid ?? 'sin-emisora'), initialValue: _emisora?.uuid, decoration: InputDecoration( labelText: l10n.favoriteStationLabel, prefixIcon: const Icon(Icons.radio_rounded), ), items: [ DropdownMenuItem( value: '', child: Text(l10n.noStationUseInternalSound), ), for (final emisora in favoritas) DropdownMenuItem( value: emisora.uuid, child: Text( localizedStationName(l10n, emisora.nombre), overflow: TextOverflow.ellipsis, ), ), ], onChanged: (uuid) => setState(() { if (uuid == null || uuid.isEmpty) { _emisora = null; return; } _emisora = favoritas.firstWhere((e) => e.uuid == uuid); }), ), if (favoritas.isEmpty) ...[ const SizedBox(height: 6), Text(l10n.saveFavoritesAlarmHint), ], if (radio.emisoraActual != null) ...[ const SizedBox(height: 8), Align( alignment: Alignment.centerLeft, child: FilledButton.tonalIcon( onPressed: () => setState(() => _emisora = radio.emisoraActual), icon: const Icon(Icons.add_task_rounded), label: Text(l10n.useCurrentStationAction), ), ), ], const SizedBox(height: 8), SwitchListTile.adaptive( contentPadding: EdgeInsets.zero, value: _sonarEnVacaciones, onChanged: (value) => setState(() => _sonarEnVacaciones = value), secondary: const _AssetIcon( 'assets/icons/alarmas/vacation_wave.png', size: 42, ), title: Text(l10n.playDuringVacations), subtitle: Text(l10n.playDuringVacationsHint), ), const SizedBox(height: 16), FilledButton.icon( onPressed: _guardar, icon: const Icon(Icons.check_rounded), label: Text(l10n.saveAlarmAction), ), ], ), ), ), ); } Future _elegirHora() async { final nueva = await showTimePicker(context: context, initialTime: _hora); if (nueva != null) setState(() => _hora = nueva); } Future _elegirFecha() async { final ahora = DateTime.now(); final nueva = await showDatePicker( context: context, initialDate: _fecha.isBefore(ahora) ? ahora : _fecha, firstDate: DateTime(ahora.year, ahora.month, ahora.day), lastDate: ahora.add(const Duration(days: 730)), ); if (nueva != null) setState(() => _fecha = nueva); } Future _guardar() async { if (_tipo == TipoProgramacionAlarma.diasSemana && _diasSemana.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context).chooseOneWeekdayError), ), ); return; } final estado = context.read(); final existente = widget.alarma; final alarma = (existente ?? estado.servicio.crearAlarma( nombre: _nombreController.text.trim(), hora: _hora.hour, minuto: _hora.minute, tipoProgramacion: _tipo, diasSemana: _diasSemana.toList()..sort(), )) .copyWith( nombre: _nombreController.text.trim().isEmpty ? AppLocalizations.of(context).defaultAlarmName : _nombreController.text.trim(), hora: _hora.hour, minuto: _hora.minute, tipoProgramacion: _tipo, diasSemana: _tipo == TipoProgramacionAlarma.diasSemana ? (_diasSemana.toList()..sort()) : const [], fechaUnica: _tipo == TipoProgramacionAlarma.unica ? _fecha : null, limpiarFechaUnica: _tipo != TipoProgramacionAlarma.unica, emisora: _emisora, sonarEnVacaciones: _sonarEnVacaciones, snoozeMinutos: existente?.snoozeMinutos ?? 5, volumen: _volumen, fadeInSegundos: _fadeInSegundos.clamp(0, 60).toInt(), sonidoInterno: _sonidoInterno, activa: true, ); await estado.guardarAlarma(alarma); if (mounted) Navigator.pop(context); } List _favoritasConSeleccion(List favoritas) { final mapa = {}; for (final emisora in favoritas) { mapa[emisora.uuid] = emisora; } final seleccionada = _emisora; if (seleccionada != null) { mapa[seleccionada.uuid] = seleccionada; } return mapa.values.toList(); } } class _AccesoDiagnostico extends StatelessWidget { const _AccesoDiagnostico({required this.estado}); final EstadoAlarmas estado; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final diag = estado.diagnostico; final exactStatus = diag?.puedeProgramarExactas == true ? l10n.statusOk : l10n.statusPending; final notificationStatus = diag?.notificacionesPermitidas == true ? l10n.statusOk : l10n.statusPending; final screenStatus = diag?.puedeUsarPantallaCompleta == true ? l10n.statusOk : l10n.statusPending; return TextButton.icon( icon: const _AssetIcon( 'assets/icons/alarmas/android_reliability.png', size: 28, ), label: Text( diag == null ? l10n.androidReliabilityTitle : l10n.androidReliabilityStatus( exactStatus, notificationStatus, screenStatus, ), ), onPressed: () async { if (diag != null && !diag.puedeProgramarExactas) { await estado.android.solicitarPermisoAlarmasExactas(); } if (diag != null && !diag.notificacionesPermitidas) { await estado.android.solicitarPermisoNotificaciones(); } if (diag != null && !diag.puedeUsarPantallaCompleta) { await estado.android.solicitarPermisoPantallaCompleta(); } await estado.cargarDiagnostico(); }, ); } } class _PanelVacaciones extends StatelessWidget { const _PanelVacaciones({required this.estado}); final EstadoAlarmas estado; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final vacaciones = [...estado.vacaciones] ..sort((a, b) => a.inicioDia.compareTo(b.inicioDia)); return PluriGlassSurface( glowColor: const Color(0xFF60A5FA).withValues(alpha: 0.22), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const _AssetIcon( 'assets/icons/alarmas/vacation_wave.png', size: 48, ), const SizedBox(width: 10), Expanded( child: Text( l10n.vacationRangesTitle, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, ), ), ), FilledButton.tonalIcon( onPressed: () => _abrirAlta(context), icon: const Icon(Icons.add_rounded), label: Text(l10n.addAction), ), ], ), const SizedBox(height: 8), Text(l10n.vacationRangesHint), if (vacaciones.isEmpty) Text(l10n.noVacationRangesLoaded) else for (final rango in vacaciones) ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.event_busy_rounded), title: Text(_nombreVisibleVacaciones(l10n, rango)), subtitle: Text( '${_fechaCorta(rango.inicioDia)} → ${_fechaCorta(rango.finDia)}', ), trailing: IconButton( tooltip: l10n.deleteRangeTooltip, onPressed: () => estado.eliminarRangoVacaciones(rango.id), icon: const Icon(Icons.delete_outline_rounded), ), ), ], ), ); } Future _abrirAlta(BuildContext context) async { await showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, builder: (_) => const _EditorVacacionesSheet(), ); } } class _EditorVacacionesSheet extends StatefulWidget { const _EditorVacacionesSheet(); @override State<_EditorVacacionesSheet> createState() => _EditorVacacionesSheetState(); } class _EditorVacacionesSheetState extends State<_EditorVacacionesSheet> { late final TextEditingController _nombreController; late DateTime _inicio; late DateTime _fin; @override void initState() { super.initState(); final hoy = DateTime.now(); _inicio = DateTime(hoy.year, hoy.month, hoy.day); _fin = _inicio.add(const Duration(days: 2)); _nombreController = TextEditingController( text: AppLocalizations.of(context).vacationsDefaultName, ); } @override void dispose() { _nombreController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final bottom = MediaQuery.of(context).viewInsets.bottom; return Padding( padding: EdgeInsets.fromLTRB(12, 12, 12, bottom + 12), child: PluriGlassSurface( borderRadius: BorderRadius.circular(28), padding: const EdgeInsets.all(18), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.newVacationRangeTitle, style: Theme.of( context, ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w900), ), const SizedBox(height: 12), TextField( controller: _nombreController, decoration: InputDecoration(labelText: l10n.nameLabel), ), const SizedBox(height: 12), Row( children: [ Expanded( child: _PickerButton( icon: Icons.play_arrow_rounded, label: l10n.startLabel, value: _fechaCorta(_inicio), onTap: () => _elegirFecha(esInicio: true), ), ), const SizedBox(width: 10), Expanded( child: _PickerButton( icon: Icons.stop_rounded, label: l10n.endLabel, value: _fechaCorta(_fin), onTap: () => _elegirFecha(esInicio: false), ), ), ], ), const SizedBox(height: 16), FilledButton.icon( onPressed: _guardar, icon: const Icon(Icons.check_rounded), label: Text(l10n.saveRangeAction), ), ], ), ), ); } Future _elegirFecha({required bool esInicio}) async { final actual = esInicio ? _inicio : _fin; final hoy = DateTime.now(); final seleccion = await showDatePicker( context: context, initialDate: actual, firstDate: DateTime(hoy.year, hoy.month, hoy.day), lastDate: hoy.add(const Duration(days: 1460)), ); if (seleccion == null) return; setState(() { if (esInicio) { _inicio = seleccion; if (_fin.isBefore(_inicio)) _fin = _inicio; } else { _fin = seleccion; } }); } Future _guardar() async { final estado = context.read(); final rango = estado.servicio.crearRangoVacaciones( inicio: _inicio, fin: _fin, nombre: _nombreController.text.trim(), ); await estado.crearRangoVacaciones(rango); if (mounted) Navigator.pop(context); } } class _AssetIcon extends StatelessWidget { const _AssetIcon(this.asset, {this.size = 44}); final String asset; final double size; @override Widget build(BuildContext context) { return Image.asset( asset, width: size, height: size, fit: BoxFit.contain, errorBuilder: (_, __, ___) => Icon(Icons.music_note_rounded, size: size * 0.65), ); } } class _PickerButton extends StatelessWidget { const _PickerButton({ required this.icon, required this.label, required this.value, required this.onTap, }); final IconData icon; final String label; final String value; final VoidCallback? onTap; @override Widget build(BuildContext context) { return OutlinedButton.icon( onPressed: onTap, icon: Icon(icon), label: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: Theme.of(context).textTheme.labelSmall), Text(value), ], ), ); } } class _SectionLabel extends StatelessWidget { const _SectionLabel({required this.icon, required this.text}); final String icon; final String text; @override Widget build(BuildContext context) { return Row( children: [ _AssetIcon(icon, size: 34), const SizedBox(width: 8), Text( text, style: Theme.of( context, ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w800), ), ], ); } } class _InfoChip extends StatelessWidget { const _InfoChip({required this.icon, required this.label}); final IconData icon; final String label; @override Widget build(BuildContext context) { return Chip(avatar: Icon(icon, size: 16), label: Text(label)); } } class _NoticeLine extends StatelessWidget { const _NoticeLine({required this.icon, required this.text}); final IconData icon; final String text; @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 18), const SizedBox(width: 8), Expanded(child: Text(text)), ], ); } } class _EmptyAlarmas extends StatelessWidget { const _EmptyAlarmas(); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return PluriGlassSurface( child: Column( children: [ const _AssetIcon('assets/icons/alarmas/alarm_music.png', size: 92), const SizedBox(height: 12), Text(l10n.noAlarmsYetTitle), const SizedBox(height: 4), Text(l10n.noAlarmsYetSubtitle), ], ), ); } } String _nombreVisibleAlarma(AppLocalizations l10n, AlarmaMusical alarma) { return localizedAlarmName(l10n, alarma.nombre); } String _nombreVisibleVacaciones(AppLocalizations l10n, RangoVacaciones rango) { return localizedVacationName(l10n, rango.nombre); } String _hora(AlarmaMusical alarma) => '${alarma.hora.toString().padLeft(2, '0')}:${alarma.minuto.toString().padLeft(2, '0')}'; String _programacion(AppLocalizations l10n, AlarmaMusical alarma) { return switch (alarma.tipoProgramacion) { TipoProgramacionAlarma.unica => l10n.alarmScheduleOnce( _fechaCorta(alarma.fechaUnica ?? DateTime.now()), ), TipoProgramacionAlarma.diaria => l10n.dailyOption, TipoProgramacionAlarma.diasSemana => l10n.alarmScheduleWeekdays( alarma.diasSemana.map((day) => _weekdayShort(l10n, day)).join(', '), ), }; } String _fechaHora(AppLocalizations l10n, DateTime fecha) => l10n.dateTimeSentence(fecha); String _weekdayShort(AppLocalizations l10n, int day) => switch (day) { DateTime.monday => l10n.weekdayShortMonday, DateTime.tuesday => l10n.weekdayShortTuesday, DateTime.wednesday => l10n.weekdayShortWednesday, DateTime.thursday => l10n.weekdayShortThursday, DateTime.friday => l10n.weekdayShortFriday, DateTime.saturday => l10n.weekdayShortSaturday, DateTime.sunday => l10n.weekdayShortSunday, _ => '?', }; String _fechaCorta(DateTime fecha) => '${fecha.day.toString().padLeft(2, '0')}/${fecha.month.toString().padLeft(2, '0')}/${fecha.year}';