feat(alarm): add musical alarm foundation
Build & Deploy Pluriwave / Análisis de código (push) Successful in 14s
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 2m45s

This commit is contained in:
2026-05-21 23:46:52 +02:00
parent 8c2cba093c
commit fb808ebb60
30 changed files with 1437 additions and 43 deletions
+227
View File
@@ -0,0 +1,227 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../estado/estado_alarmas.dart';
import '../modelos/alarma_musical.dart';
import '../widgets/pluri_glass_surface.dart';
import '../widgets/pluri_icon.dart';
import '../widgets/pluri_premium_widgets.dart';
class PantallaAlarmas extends StatelessWidget {
const PantallaAlarmas({super.key});
@override
Widget build(BuildContext context) {
final estado = context.watch<EstadoAlarmas>();
return ListView(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 124),
children: [
PluriScreenHeader(
title: 'Alarmas musicales',
subtitle:
'Despertador con radio, vacaciones, aviso previo y fallbacks seguros.',
glyph: PluriIconGlyph.alarm,
primaryActionLabel: 'Nueva alarma',
onPrimaryAction: () => _crearDemo(context),
trailing: PluriStatusPill(
icon: Icons.alarm_on_rounded,
label: '${estado.alarmas.length} alarmas',
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
_DiagnosticoAlarmas(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: 10),
],
],
),
),
],
);
}
Future<void> _crearDemo(BuildContext context) async {
final estado = context.read<EstadoAlarmas>();
final ahora = TimeOfDay.now();
final alarma = estado.servicio.crearAlarma(
nombre: 'Despertador musical',
hora: ahora.hour,
minuto: (ahora.minute + 2) % 60,
tipoProgramacion: TipoProgramacionAlarma.diaria,
diasSemana: const [],
);
await estado.guardarAlarma(alarma);
}
}
class _DiagnosticoAlarmas extends StatelessWidget {
const _DiagnosticoAlarmas({required this.estado});
final EstadoAlarmas estado;
@override
Widget build(BuildContext context) {
final diag = estado.diagnostico;
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.health_and_safety_outlined),
const SizedBox(width: 12),
Text(
'Fiabilidad Android',
style: Theme.of(context).textTheme.titleMedium,
),
const Spacer(),
IconButton(
tooltip: 'Revisar',
icon: const Icon(Icons.refresh_rounded),
onPressed: estado.cargarDiagnostico,
),
],
),
const SizedBox(height: 8),
_EstadoPermiso(
label: 'Alarmas exactas',
ok: diag?.puedeProgramarExactas ?? false,
),
_EstadoPermiso(
label: 'Notificaciones',
ok: diag?.notificacionesPermitidas ?? false,
),
if (diag == null)
Text(
'Diagnóstico pendiente. En Android se revisan permisos nativos.',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
}
class _EstadoPermiso extends StatelessWidget {
const _EstadoPermiso({required this.label, required this.ok});
final String label;
final bool ok;
@override
Widget build(BuildContext context) {
return ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: Icon(
ok ? Icons.check_circle_rounded : Icons.warning_amber_rounded,
color: ok ? Colors.greenAccent : Colors.orangeAccent,
),
title: Text(label),
subtitle: Text(ok ? 'Correcto' : 'Requiere revisión'),
);
}
}
class _TarjetaAlarma extends StatelessWidget {
const _TarjetaAlarma({required this.alarma});
final AlarmaMusical alarma;
@override
Widget build(BuildContext context) {
final estado = context.read<EstadoAlarmas>();
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
value: alarma.activa,
onChanged: (value) => estado.cambiarActiva(alarma, value),
title: Text(
_hora(alarma),
style: Theme.of(context).textTheme.headlineMedium,
),
subtitle: Text(alarma.nombre),
),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
Chip(label: Text(_programacion(alarma))),
Chip(label: Text('Snooze ${alarma.snoozeMinutos} min')),
Chip(
label: Text(
alarma.sonarEnVacaciones
? 'Suena en vacaciones'
: 'Respeta vacaciones',
),
),
],
),
if (alarma.proximaEjecucion != null) ...[
const SizedBox(height: 8),
Text('Próxima: ${alarma.proximaEjecucion!.toLocal()}'),
],
const SizedBox(height: 8),
Row(
children: [
TextButton.icon(
icon: const Icon(Icons.skip_next_rounded),
label: const Text('Saltar próxima'),
onPressed: () => estado.saltarProxima(alarma.id),
),
const Spacer(),
IconButton(
tooltip: 'Eliminar',
icon: const Icon(Icons.delete_outline_rounded),
onPressed: () => estado.eliminarAlarma(alarma.id),
),
],
),
],
),
);
}
String _hora(AlarmaMusical alarma) =>
'${alarma.hora.toString().padLeft(2, '0')}:${alarma.minuto.toString().padLeft(2, '0')}';
String _programacion(AlarmaMusical alarma) {
return switch (alarma.tipoProgramacion) {
TipoProgramacionAlarma.unica => 'Una vez',
TipoProgramacionAlarma.diaria => 'Diaria',
TipoProgramacionAlarma.diasSemana =>
'Días: ${alarma.diasSemana.join(', ')}',
};
}
}
class _EmptyAlarmas extends StatelessWidget {
const _EmptyAlarmas();
@override
Widget build(BuildContext context) {
return const PluriGlassSurface(
child: Column(
children: [
Icon(Icons.alarm_add_rounded, size: 42),
SizedBox(height: 12),
Text('Todavía no hay alarmas.'),
SizedBox(height: 4),
Text('Crea una para empezar a diseñar tu despertar musical.'),
],
),
);
}
}