feat(alarm): complete musical alarm flows
This commit is contained in:
@@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'estado/estado_radio.dart';
|
||||
import 'estado/estado_alarmas.dart';
|
||||
import 'modelos/alarma_musical.dart';
|
||||
import 'pantallas/pantalla_alarmas.dart';
|
||||
import 'pantallas/pantalla_alarma_sonando.dart';
|
||||
import 'pantallas/pantalla_inicio.dart';
|
||||
import 'pantallas/pantalla_buscar.dart';
|
||||
import 'pantallas/pantalla_favoritos.dart';
|
||||
@@ -13,6 +15,7 @@ import 'widgets/pluri_glass_surface.dart';
|
||||
import 'widgets/pluri_icon.dart';
|
||||
import 'widgets/pluri_wave_scaffold.dart';
|
||||
import 'package:pluriwave/widgets/mini_reproductor.dart';
|
||||
import 'servicios/servicio_alarmas_android.dart';
|
||||
|
||||
class PluriWaveApp extends StatelessWidget {
|
||||
const PluriWaveApp({super.key});
|
||||
@@ -46,7 +49,9 @@ class _PaginaPrincipal extends StatefulWidget {
|
||||
class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
int _indice = 0;
|
||||
StreamSubscription<String>? _errorSubscription;
|
||||
StreamSubscription<EventoAlarmaAndroid>? _alarmaSubscription;
|
||||
EstadoRadio? _estadoSuscrito;
|
||||
bool _alarmaInicialProcesada = false;
|
||||
|
||||
static const _paginas = [
|
||||
PantallaInicio(),
|
||||
@@ -118,11 +123,22 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
final alarmas = context.read<EstadoAlarmas>();
|
||||
_alarmaSubscription ??= alarmas.android.eventosAlarma.listen((evento) {
|
||||
if (!mounted) return;
|
||||
_abrirAlarmaSonando(evento);
|
||||
});
|
||||
if (!_alarmaInicialProcesada) {
|
||||
_alarmaInicialProcesada = true;
|
||||
unawaited(_procesarAlarmaInicial(alarmas));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_errorSubscription?.cancel();
|
||||
_alarmaSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -165,6 +181,47 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _procesarAlarmaInicial(EstadoAlarmas alarmas) async {
|
||||
final evento = await alarmas.android.obtenerEventoInicial();
|
||||
if (evento != null && mounted) {
|
||||
await _abrirAlarmaSonando(evento);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _abrirAlarmaSonando(EventoAlarmaAndroid evento) async {
|
||||
final estado = context.read<EstadoAlarmas>();
|
||||
await estado.refrescarProgramacion();
|
||||
AlarmaMusical? alarma;
|
||||
for (final item in estado.alarmas) {
|
||||
if (item.id == evento.alarmaId) {
|
||||
alarma = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (alarma == null || !mounted) return;
|
||||
if (evento.accion.endsWith('.SKIP_NEXT')) {
|
||||
await estado.saltarProxima(alarma.id);
|
||||
if (!mounted) return;
|
||||
setState(() => _indice = 3);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Omitida esta ejecución de ${alarma.nombre}.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (evento.accion.endsWith('.PRE_NOTICE')) {
|
||||
setState(() => _indice = 3);
|
||||
return;
|
||||
}
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => PantallaAlarmaSonando(alarma: alarma!),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _mostrarTimerDialog(BuildContext context) {
|
||||
final estado = context.read<EstadoRadio>();
|
||||
showModalBottomSheet(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../modelos/alarma_musical.dart';
|
||||
@@ -21,12 +23,15 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
|
||||
List<AlarmaMusical> _alarmas = [];
|
||||
List<RangoVacaciones> _vacaciones = [];
|
||||
List<ExcepcionAlarma> _excepciones = [];
|
||||
DiagnosticoAlarmasAndroid? _diagnostico;
|
||||
Timer? _refresco;
|
||||
bool _cargando = false;
|
||||
String? _error;
|
||||
|
||||
List<AlarmaMusical> get alarmas => List.unmodifiable(_alarmas);
|
||||
List<RangoVacaciones> get vacaciones => List.unmodifiable(_vacaciones);
|
||||
List<ExcepcionAlarma> get excepciones => List.unmodifiable(_excepciones);
|
||||
DiagnosticoAlarmasAndroid? get diagnostico => _diagnostico;
|
||||
bool get cargando => _cargando;
|
||||
String? get error => _error;
|
||||
@@ -47,6 +52,7 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
_aplicar(config);
|
||||
await _sincronizarTodas();
|
||||
await cargarDiagnostico();
|
||||
_activarRefresco();
|
||||
} catch (e) {
|
||||
_error = 'No se pudieron cargar las alarmas: $e';
|
||||
} finally {
|
||||
@@ -62,6 +68,13 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> refrescarProgramacion() async {
|
||||
final config = await servicio.recalcularTodas();
|
||||
_aplicar(config);
|
||||
await _sincronizarTodas();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> eliminarAlarma(String id) async {
|
||||
final config = await servicio.eliminarAlarma(id);
|
||||
_aplicar(config);
|
||||
@@ -96,6 +109,32 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> posponerAlarma(AlarmaMusical alarma, int minutos) async {
|
||||
final proxima = DateTime.now().add(Duration(minutes: minutos));
|
||||
await android.programar(alarma.copyWith(proximaEjecucion: proxima));
|
||||
}
|
||||
|
||||
Future<void> finalizarEjecucion(String alarmaId) async {
|
||||
await refrescarProgramacion();
|
||||
}
|
||||
|
||||
Future<void> crearRangoVacaciones(RangoVacaciones rango) async {
|
||||
final nuevos = [..._vacaciones, rango];
|
||||
await guardarVacaciones(nuevos);
|
||||
}
|
||||
|
||||
Future<void> eliminarRangoVacaciones(String id) async {
|
||||
final nuevos = _vacaciones.where((v) => v.id != id).toList();
|
||||
await guardarVacaciones(nuevos);
|
||||
}
|
||||
|
||||
ExcepcionAlarma? ultimaExcepcionPara(String alarmaId) {
|
||||
final candidatas =
|
||||
_excepciones.where((e) => e.alarmaId == alarmaId).toList()
|
||||
..sort((a, b) => b.ejecucion.compareTo(a.ejecucion));
|
||||
return candidatas.isEmpty ? null : candidatas.first;
|
||||
}
|
||||
|
||||
Future<void> cargarDiagnostico() async {
|
||||
try {
|
||||
_diagnostico = await android.diagnostico();
|
||||
@@ -114,5 +153,19 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
void _aplicar(ConfiguracionAlarmas config) {
|
||||
_alarmas = config.alarmas;
|
||||
_vacaciones = config.vacaciones;
|
||||
_excepciones = config.excepciones;
|
||||
}
|
||||
|
||||
void _activarRefresco() {
|
||||
_refresco?.cancel();
|
||||
_refresco = Timer.periodic(const Duration(minutes: 1), (_) {
|
||||
refrescarProgramacion();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refresco?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class AlarmaMusical {
|
||||
required this.minuto,
|
||||
required this.tipoProgramacion,
|
||||
required this.diasSemana,
|
||||
this.fechaUnica,
|
||||
this.emisora,
|
||||
this.emisoraFallback,
|
||||
this.activa = true,
|
||||
@@ -31,6 +32,7 @@ class AlarmaMusical {
|
||||
final int minuto;
|
||||
final TipoProgramacionAlarma tipoProgramacion;
|
||||
final List<int> diasSemana;
|
||||
final DateTime? fechaUnica;
|
||||
final Emisora? emisora;
|
||||
final Emisora? emisoraFallback;
|
||||
final bool sonarEnVacaciones;
|
||||
@@ -49,6 +51,8 @@ class AlarmaMusical {
|
||||
int? minuto,
|
||||
TipoProgramacionAlarma? tipoProgramacion,
|
||||
List<int>? diasSemana,
|
||||
DateTime? fechaUnica,
|
||||
bool limpiarFechaUnica = false,
|
||||
Emisora? emisora,
|
||||
Emisora? emisoraFallback,
|
||||
bool? sonarEnVacaciones,
|
||||
@@ -67,6 +71,7 @@ class AlarmaMusical {
|
||||
minuto: minuto ?? this.minuto,
|
||||
tipoProgramacion: tipoProgramacion ?? this.tipoProgramacion,
|
||||
diasSemana: diasSemana ?? this.diasSemana,
|
||||
fechaUnica: limpiarFechaUnica ? null : fechaUnica ?? this.fechaUnica,
|
||||
emisora: emisora ?? this.emisora,
|
||||
emisoraFallback: emisoraFallback ?? this.emisoraFallback,
|
||||
sonarEnVacaciones: sonarEnVacaciones ?? this.sonarEnVacaciones,
|
||||
@@ -87,6 +92,7 @@ class AlarmaMusical {
|
||||
'minuto': minuto,
|
||||
'tipoProgramacion': tipoProgramacion.name,
|
||||
'diasSemana': diasSemana,
|
||||
'fechaUnica': fechaUnica?.toIso8601String(),
|
||||
'emisora': emisora?.toMap(),
|
||||
'emisoraFallback': emisoraFallback?.toMap(),
|
||||
'sonarEnVacaciones': sonarEnVacaciones,
|
||||
@@ -115,6 +121,7 @@ class AlarmaMusical {
|
||||
.whereType<int>()
|
||||
.where((d) => d >= DateTime.monday && d <= DateTime.sunday)
|
||||
.toList(),
|
||||
fechaUnica: _dateFromJson(json['fechaUnica']),
|
||||
emisora: _emisoraFromJson(json['emisora']),
|
||||
emisoraFallback: _emisoraFromJson(json['emisoraFallback']),
|
||||
sonarEnVacaciones: json['sonarEnVacaciones'] as bool? ?? true,
|
||||
@@ -166,13 +173,27 @@ class RangoVacaciones {
|
||||
final DateTime fin;
|
||||
final bool activo;
|
||||
|
||||
DateTime get inicioDia => DateTime(inicio.year, inicio.month, inicio.day);
|
||||
DateTime get finDia => DateTime(fin.year, fin.month, fin.day);
|
||||
|
||||
bool contiene(DateTime fecha) {
|
||||
final dia = DateTime(fecha.year, fecha.month, fecha.day);
|
||||
final desde = DateTime(inicio.year, inicio.month, inicio.day);
|
||||
final hasta = DateTime(fin.year, fin.month, fin.day);
|
||||
final desde = inicioDia;
|
||||
final hasta = finDia;
|
||||
return activo && !dia.isBefore(desde) && !dia.isAfter(hasta);
|
||||
}
|
||||
|
||||
RangoVacaciones normalizado() {
|
||||
if (!finDia.isBefore(inicioDia)) return this;
|
||||
return RangoVacaciones(
|
||||
id: id,
|
||||
nombre: nombre,
|
||||
inicio: finDia,
|
||||
fin: inicioDia,
|
||||
activo: activo,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'nombre': nombre,
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../estado/estado_alarmas.dart';
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../modelos/alarma_musical.dart';
|
||||
import '../servicios/servicio_audio.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
|
||||
class PantallaAlarmaSonando extends StatefulWidget {
|
||||
const PantallaAlarmaSonando({super.key, required this.alarma});
|
||||
|
||||
final AlarmaMusical alarma;
|
||||
|
||||
@override
|
||||
State<PantallaAlarmaSonando> createState() => _PantallaAlarmaSonandoState();
|
||||
}
|
||||
|
||||
class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
final AudioPlayer _fallbackPlayer = AudioPlayer();
|
||||
StreamSubscription<EstadoReproduccion>? _estadoSub;
|
||||
Timer? _fallbackTimer;
|
||||
bool _fallbackActivo = false;
|
||||
bool _radioIntentada = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _iniciarAlarma());
|
||||
}
|
||||
|
||||
Future<void> _iniciarAlarma() async {
|
||||
final radio = context.read<EstadoRadio>();
|
||||
await _fallbackPlayer.setVolume(widget.alarma.volumen.clamp(0.0, 1.0));
|
||||
await _fallbackPlayer.setLoopMode(LoopMode.one);
|
||||
|
||||
final emisora = widget.alarma.emisora;
|
||||
if (emisora == null) {
|
||||
await _iniciarFallback();
|
||||
return;
|
||||
}
|
||||
|
||||
_radioIntentada = true;
|
||||
await radio.audio.setVolumen(widget.alarma.volumen.clamp(0.0, 1.0));
|
||||
unawaited(radio.reproducir(emisora));
|
||||
|
||||
_estadoSub = radio.estadoStream.listen((estado) {
|
||||
if (estado == EstadoReproduccion.reproduciendo && mounted) {
|
||||
_fallbackTimer?.cancel();
|
||||
}
|
||||
if (estado == EstadoReproduccion.error && mounted) {
|
||||
_iniciarFallback();
|
||||
}
|
||||
});
|
||||
|
||||
_fallbackTimer = Timer(const Duration(seconds: 12), () {
|
||||
if (mounted) _iniciarFallback();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _iniciarFallback() async {
|
||||
if (_fallbackActivo) return;
|
||||
_fallbackActivo = true;
|
||||
await _fallbackPlayer.setAsset(_assetFallback(widget.alarma.sonidoInterno));
|
||||
await _fallbackPlayer.play();
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _detener() async {
|
||||
final radio = context.read<EstadoRadio>();
|
||||
final alarmas = context.read<EstadoAlarmas>();
|
||||
final navigator = Navigator.of(context);
|
||||
_fallbackTimer?.cancel();
|
||||
await _estadoSub?.cancel();
|
||||
await _fallbackPlayer.stop();
|
||||
await radio.audio.pausar();
|
||||
await alarmas.finalizarEjecucion(widget.alarma.id);
|
||||
if (mounted) navigator.pop();
|
||||
}
|
||||
|
||||
Future<void> _posponer(int minutos) async {
|
||||
final radio = context.read<EstadoRadio>();
|
||||
final alarmas = context.read<EstadoAlarmas>();
|
||||
final navigator = Navigator.of(context);
|
||||
_fallbackTimer?.cancel();
|
||||
await _estadoSub?.cancel();
|
||||
await _fallbackPlayer.stop();
|
||||
await radio.audio.pausar();
|
||||
await alarmas.posponerAlarma(widget.alarma, minutos);
|
||||
if (mounted) navigator.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fallbackTimer?.cancel();
|
||||
_estadoSub?.cancel();
|
||||
_fallbackPlayer.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final alarma = widget.alarma;
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF061722),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Center(
|
||||
child: PluriGlassSurface(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
padding: const EdgeInsets.all(24),
|
||||
glowColor: const Color(0xFFFFB86B).withValues(alpha: 0.35),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/icons/alarmas/alarm_music.png',
|
||||
width: 128,
|
||||
height: 128,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_hora(alarma),
|
||||
style: Theme.of(context).textTheme.displayMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
alarma.nombre,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_fallbackActivo
|
||||
? 'Sonando con audio seguro interno.'
|
||||
: _radioIntentada
|
||||
? 'Intentando reproducir tu emisora con máxima calidad disponible.'
|
||||
: 'Preparando audio seguro interno.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
FilledButton.icon(
|
||||
onPressed: _detener,
|
||||
icon: const Icon(Icons.stop_rounded),
|
||||
label: const Text('Detener alarma'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final min in const [3, 5, 10])
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.snooze_rounded, size: 18),
|
||||
label: Text('Posponer $min min'),
|
||||
onPressed: () => _posponer(min),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _assetFallback(SonidoInternoAlarma sonido) => switch (sonido) {
|
||||
SonidoInternoAlarma.amanecer => 'assets/audio/alarm_amanecer.wav',
|
||||
SonidoInternoAlarma.campanaSuave =>
|
||||
'assets/audio/alarm_campana_suave.wav',
|
||||
SonidoInternoAlarma.pulsoDigital => 'assets/audio/alarm_pulso_digital.wav',
|
||||
};
|
||||
|
||||
String _hora(AlarmaMusical alarma) =>
|
||||
'${alarma.hora.toString().padLeft(2, '0')}:${alarma.minuto.toString().padLeft(2, '0')}';
|
||||
+818
-131
File diff suppressed because it is too large
Load Diff
@@ -122,13 +122,49 @@ class ServicioAlarmas {
|
||||
List<RangoVacaciones> vacaciones,
|
||||
) async {
|
||||
final config = await cargar();
|
||||
final normalizadas =
|
||||
vacaciones
|
||||
.map((v) => v.normalizado())
|
||||
.toList()
|
||||
..sort((a, b) => a.inicioDia.compareTo(b.inicioDia));
|
||||
final alarmas =
|
||||
config.alarmas
|
||||
.map((a) => _recalcular(a, vacaciones, config.excepciones))
|
||||
.map((a) => _recalcular(a, normalizadas, config.excepciones))
|
||||
.toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
vacaciones: vacaciones,
|
||||
vacaciones: normalizadas,
|
||||
excepciones: config.excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
|
||||
RangoVacaciones crearRangoVacaciones({
|
||||
required DateTime inicio,
|
||||
required DateTime fin,
|
||||
String? nombre,
|
||||
}) {
|
||||
final rango = RangoVacaciones(
|
||||
id: _uuid.v4(),
|
||||
nombre: (nombre == null || nombre.trim().isEmpty)
|
||||
? 'Vacaciones'
|
||||
: nombre.trim(),
|
||||
inicio: inicio,
|
||||
fin: fin,
|
||||
);
|
||||
return rango.normalizado();
|
||||
}
|
||||
|
||||
Future<ConfiguracionAlarmas> recalcularTodas() async {
|
||||
final config = await cargar();
|
||||
final alarmas =
|
||||
config.alarmas
|
||||
.map((a) => _recalcular(a, config.vacaciones, config.excepciones))
|
||||
.toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: config.excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
@@ -169,7 +205,13 @@ class ServicioAlarmas {
|
||||
required int minuto,
|
||||
required TipoProgramacionAlarma tipoProgramacion,
|
||||
required List<int> diasSemana,
|
||||
DateTime? fechaUnica,
|
||||
Emisora? emisora,
|
||||
Emisora? emisoraFallback,
|
||||
bool sonarEnVacaciones = true,
|
||||
int snoozeMinutos = 5,
|
||||
double volumen = 0.85,
|
||||
SonidoInternoAlarma sonidoInterno = SonidoInternoAlarma.amanecer,
|
||||
}) {
|
||||
final ahora = _reloj();
|
||||
return AlarmaMusical(
|
||||
@@ -179,7 +221,13 @@ class ServicioAlarmas {
|
||||
minuto: minuto,
|
||||
tipoProgramacion: tipoProgramacion,
|
||||
diasSemana: diasSemana,
|
||||
fechaUnica: fechaUnica,
|
||||
emisora: emisora,
|
||||
emisoraFallback: emisoraFallback,
|
||||
sonarEnVacaciones: sonarEnVacaciones,
|
||||
snoozeMinutos: snoozeMinutos,
|
||||
volumen: volumen,
|
||||
sonidoInterno: sonidoInterno,
|
||||
creadaEn: ahora,
|
||||
actualizadaEn: ahora,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../modelos/alarma_musical.dart';
|
||||
|
||||
class EventoAlarmaAndroid {
|
||||
const EventoAlarmaAndroid({
|
||||
required this.alarmaId,
|
||||
required this.titulo,
|
||||
required this.accion,
|
||||
});
|
||||
|
||||
final String alarmaId;
|
||||
final String titulo;
|
||||
final String accion;
|
||||
|
||||
factory EventoAlarmaAndroid.fromMap(Map<Object?, Object?> map) {
|
||||
return EventoAlarmaAndroid(
|
||||
alarmaId: map['alarmId'] as String? ?? '',
|
||||
titulo: map['alarmTitle'] as String? ?? 'PluriWave',
|
||||
accion: map['alarmAction'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DiagnosticoAlarmasAndroid {
|
||||
const DiagnosticoAlarmasAndroid({
|
||||
required this.puedeProgramarExactas,
|
||||
@@ -28,9 +50,16 @@ class DiagnosticoAlarmasAndroid {
|
||||
class ServicioAlarmasAndroid {
|
||||
ServicioAlarmasAndroid({
|
||||
MethodChannel channel = const MethodChannel('pluriwave/alarm_scheduler'),
|
||||
}) : _channel = channel;
|
||||
}) : _channel = channel {
|
||||
_instalarHandler(_channel);
|
||||
}
|
||||
|
||||
final MethodChannel _channel;
|
||||
static final _eventosController =
|
||||
StreamController<EventoAlarmaAndroid>.broadcast();
|
||||
static bool _handlerInstalado = false;
|
||||
|
||||
Stream<EventoAlarmaAndroid> get eventosAlarma => _eventosController.stream;
|
||||
|
||||
Future<void> programar(AlarmaMusical alarma) async {
|
||||
final proxima = alarma.proximaEjecucion;
|
||||
@@ -56,4 +85,28 @@ class ServicioAlarmasAndroid {
|
||||
);
|
||||
return DiagnosticoAlarmasAndroid.fromMap(raw ?? const {});
|
||||
}
|
||||
|
||||
Future<EventoAlarmaAndroid?> obtenerEventoInicial() async {
|
||||
final raw = await _channel.invokeMethod<Map<Object?, Object?>>(
|
||||
'getInitialAlarmIntent',
|
||||
);
|
||||
if (raw == null || raw.isEmpty) return null;
|
||||
final evento = EventoAlarmaAndroid.fromMap(raw);
|
||||
return evento.alarmaId.isEmpty ? null : evento;
|
||||
}
|
||||
|
||||
static void _instalarHandler(MethodChannel channel) {
|
||||
if (_handlerInstalado) return;
|
||||
_handlerInstalado = true;
|
||||
channel.setMethodCallHandler((call) async {
|
||||
if (call.method != 'alarmFired') return;
|
||||
final args = call.arguments;
|
||||
if (args is Map) {
|
||||
final evento = EventoAlarmaAndroid.fromMap(args);
|
||||
if (evento.alarmaId.isNotEmpty) {
|
||||
_eventosController.add(evento);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,29 @@ class ServicioProgramacionAlarmas {
|
||||
}) {
|
||||
if (!alarma.activa) return null;
|
||||
|
||||
final diaBase =
|
||||
alarma.tipoProgramacion == TipoProgramacionAlarma.unica &&
|
||||
alarma.fechaUnica != null
|
||||
? alarma.fechaUnica!
|
||||
: desde;
|
||||
final inicio = DateTime(
|
||||
desde.year,
|
||||
desde.month,
|
||||
desde.day,
|
||||
diaBase.year,
|
||||
diaBase.month,
|
||||
diaBase.day,
|
||||
alarma.hora,
|
||||
alarma.minuto,
|
||||
);
|
||||
final primerCandidato =
|
||||
inicio.isAfter(desde) ? inicio : inicio.add(const Duration(days: 1));
|
||||
alarma.tipoProgramacion == TipoProgramacionAlarma.unica
|
||||
? inicio
|
||||
: inicio.isAfter(desde)
|
||||
? inicio
|
||||
: inicio.add(const Duration(days: 1));
|
||||
|
||||
return switch (alarma.tipoProgramacion) {
|
||||
TipoProgramacionAlarma.unica =>
|
||||
_esValida(alarma, primerCandidato, vacaciones, excepciones)
|
||||
primerCandidato.isAfter(desde) &&
|
||||
_esValida(alarma, primerCandidato, vacaciones, excepciones)
|
||||
? primerCandidato
|
||||
: null,
|
||||
TipoProgramacionAlarma.diaria => _buscarDiaria(
|
||||
|
||||
Reference in New Issue
Block a user