feat(alarm): add musical alarm foundation
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../modelos/alarma_musical.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import 'servicio_programacion_alarmas.dart';
|
||||
|
||||
class ConfiguracionAlarmas {
|
||||
const ConfiguracionAlarmas({
|
||||
required this.alarmas,
|
||||
required this.vacaciones,
|
||||
required this.excepciones,
|
||||
});
|
||||
|
||||
final List<AlarmaMusical> alarmas;
|
||||
final List<RangoVacaciones> vacaciones;
|
||||
final List<ExcepcionAlarma> excepciones;
|
||||
}
|
||||
|
||||
class ServicioAlarmas {
|
||||
ServicioAlarmas({
|
||||
ServicioProgramacionAlarmas? programacion,
|
||||
SharedPreferences? prefs,
|
||||
DateTime Function()? reloj,
|
||||
}) : _programacion = programacion ?? ServicioProgramacionAlarmas(),
|
||||
_prefs = prefs,
|
||||
_reloj = reloj ?? DateTime.now;
|
||||
|
||||
static const _keyConfig = 'alarmas_musicales_v1';
|
||||
final ServicioProgramacionAlarmas _programacion;
|
||||
final SharedPreferences? _prefs;
|
||||
final DateTime Function() _reloj;
|
||||
final _uuid = const Uuid();
|
||||
|
||||
Future<ConfiguracionAlarmas> cargar() async {
|
||||
final prefs = await _resolverPrefs();
|
||||
final raw = prefs.getString(_keyConfig);
|
||||
if (raw == null || raw.trim().isEmpty) {
|
||||
return const ConfiguracionAlarmas(
|
||||
alarmas: [],
|
||||
vacaciones: [],
|
||||
excepciones: [],
|
||||
);
|
||||
}
|
||||
try {
|
||||
final data = jsonDecode(raw) as Map<String, dynamic>;
|
||||
return ConfiguracionAlarmas(
|
||||
alarmas:
|
||||
(data['alarmas'] as List? ?? const [])
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(e) => AlarmaMusical.fromJson(Map<String, dynamic>.from(e)),
|
||||
)
|
||||
.toList(),
|
||||
vacaciones:
|
||||
(data['vacaciones'] as List? ?? const [])
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(e) => RangoVacaciones.fromJson(Map<String, dynamic>.from(e)),
|
||||
)
|
||||
.toList(),
|
||||
excepciones:
|
||||
(data['excepciones'] as List? ?? const [])
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(e) => ExcepcionAlarma.fromJson(Map<String, dynamic>.from(e)),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
} catch (_) {
|
||||
return const ConfiguracionAlarmas(
|
||||
alarmas: [],
|
||||
vacaciones: [],
|
||||
excepciones: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ConfiguracionAlarmas> guardarAlarma(
|
||||
AlarmaMusical alarma, {
|
||||
List<RangoVacaciones>? vacaciones,
|
||||
List<ExcepcionAlarma>? excepciones,
|
||||
}) async {
|
||||
final config = await cargar();
|
||||
final ahora = _reloj();
|
||||
final alarmas = List<AlarmaMusical>.from(config.alarmas);
|
||||
final index = alarmas.indexWhere((a) => a.id == alarma.id);
|
||||
final normalizada = _recalcular(
|
||||
alarma.copyWith(creadaEn: alarma.creadaEn ?? ahora, actualizadaEn: ahora),
|
||||
vacaciones ?? config.vacaciones,
|
||||
excepciones ?? config.excepciones,
|
||||
);
|
||||
|
||||
if (index >= 0) {
|
||||
alarmas[index] = normalizada;
|
||||
} else {
|
||||
alarmas.add(normalizada);
|
||||
}
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
vacaciones: vacaciones ?? config.vacaciones,
|
||||
excepciones: excepciones ?? config.excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
|
||||
Future<ConfiguracionAlarmas> eliminarAlarma(String id) async {
|
||||
final config = await cargar();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: config.alarmas.where((a) => a.id != id).toList(),
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: config.excepciones.where((e) => e.alarmaId != id).toList(),
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
|
||||
Future<ConfiguracionAlarmas> guardarVacaciones(
|
||||
List<RangoVacaciones> vacaciones,
|
||||
) async {
|
||||
final config = await cargar();
|
||||
final alarmas =
|
||||
config.alarmas
|
||||
.map((a) => _recalcular(a, vacaciones, config.excepciones))
|
||||
.toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
vacaciones: vacaciones,
|
||||
excepciones: config.excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
|
||||
Future<ConfiguracionAlarmas> saltarProxima(String alarmaId) async {
|
||||
final config = await cargar();
|
||||
final alarma = config.alarmas.firstWhere((a) => a.id == alarmaId);
|
||||
final proxima = alarma.proximaEjecucion;
|
||||
if (proxima == null) return config;
|
||||
|
||||
final excepciones = [
|
||||
...config.excepciones,
|
||||
ExcepcionAlarma(alarmaId: alarmaId, ejecucion: proxima, tipo: 'skipNext'),
|
||||
];
|
||||
final alarmas =
|
||||
config.alarmas
|
||||
.map(
|
||||
(a) =>
|
||||
a.id == alarmaId
|
||||
? _recalcular(a, config.vacaciones, excepciones)
|
||||
: a,
|
||||
)
|
||||
.toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
|
||||
AlarmaMusical crearAlarma({
|
||||
required String nombre,
|
||||
required int hora,
|
||||
required int minuto,
|
||||
required TipoProgramacionAlarma tipoProgramacion,
|
||||
required List<int> diasSemana,
|
||||
Emisora? emisora,
|
||||
}) {
|
||||
final ahora = _reloj();
|
||||
return AlarmaMusical(
|
||||
id: _uuid.v4(),
|
||||
nombre: nombre,
|
||||
hora: hora,
|
||||
minuto: minuto,
|
||||
tipoProgramacion: tipoProgramacion,
|
||||
diasSemana: diasSemana,
|
||||
emisora: emisora,
|
||||
creadaEn: ahora,
|
||||
actualizadaEn: ahora,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _guardar(ConfiguracionAlarmas config) async {
|
||||
final prefs = await _resolverPrefs();
|
||||
await prefs.setString(
|
||||
_keyConfig,
|
||||
jsonEncode({
|
||||
'alarmas': config.alarmas.map((a) => a.toJson()).toList(),
|
||||
'vacaciones': config.vacaciones.map((v) => v.toJson()).toList(),
|
||||
'excepciones': config.excepciones.map((e) => e.toJson()).toList(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
AlarmaMusical _recalcular(
|
||||
AlarmaMusical alarma,
|
||||
List<RangoVacaciones> vacaciones,
|
||||
List<ExcepcionAlarma> excepciones,
|
||||
) {
|
||||
return alarma.copyWith(
|
||||
proximaEjecucion: _programacion.calcularProxima(
|
||||
alarma: alarma,
|
||||
desde: _reloj(),
|
||||
vacaciones: vacaciones,
|
||||
excepciones: excepciones,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<SharedPreferences> _resolverPrefs() async =>
|
||||
_prefs ?? SharedPreferences.getInstance();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../modelos/alarma_musical.dart';
|
||||
|
||||
class DiagnosticoAlarmasAndroid {
|
||||
const DiagnosticoAlarmasAndroid({
|
||||
required this.puedeProgramarExactas,
|
||||
required this.notificacionesPermitidas,
|
||||
required this.fabricante,
|
||||
required this.versionSdk,
|
||||
});
|
||||
|
||||
final bool puedeProgramarExactas;
|
||||
final bool notificacionesPermitidas;
|
||||
final String fabricante;
|
||||
final int versionSdk;
|
||||
|
||||
factory DiagnosticoAlarmasAndroid.fromMap(Map<Object?, Object?> map) {
|
||||
return DiagnosticoAlarmasAndroid(
|
||||
puedeProgramarExactas: map['canScheduleExactAlarms'] as bool? ?? true,
|
||||
notificacionesPermitidas: map['notificationsEnabled'] as bool? ?? true,
|
||||
fabricante: map['manufacturer'] as String? ?? 'Android',
|
||||
versionSdk: map['sdkInt'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ServicioAlarmasAndroid {
|
||||
ServicioAlarmasAndroid({
|
||||
MethodChannel channel = const MethodChannel('pluriwave/alarm_scheduler'),
|
||||
}) : _channel = channel;
|
||||
|
||||
final MethodChannel _channel;
|
||||
|
||||
Future<void> programar(AlarmaMusical alarma) async {
|
||||
final proxima = alarma.proximaEjecucion;
|
||||
if (proxima == null || !alarma.activa) {
|
||||
await cancelar(alarma.id);
|
||||
return;
|
||||
}
|
||||
await _channel.invokeMethod<void>('scheduleAlarm', {
|
||||
'id': alarma.id,
|
||||
'title': alarma.nombre,
|
||||
'triggerAtMillis': proxima.millisecondsSinceEpoch,
|
||||
'preNoticeAtMillis':
|
||||
proxima.subtract(const Duration(minutes: 30)).millisecondsSinceEpoch,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> cancelar(String alarmaId) =>
|
||||
_channel.invokeMethod<void>('cancelAlarm', {'id': alarmaId});
|
||||
|
||||
Future<DiagnosticoAlarmasAndroid> diagnostico() async {
|
||||
final raw = await _channel.invokeMethod<Map<Object?, Object?>>(
|
||||
'diagnostics',
|
||||
);
|
||||
return DiagnosticoAlarmasAndroid.fromMap(raw ?? const {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import '../modelos/alarma_musical.dart';
|
||||
|
||||
class ServicioProgramacionAlarmas {
|
||||
DateTime? calcularProxima({
|
||||
required AlarmaMusical alarma,
|
||||
required DateTime desde,
|
||||
List<RangoVacaciones> vacaciones = const [],
|
||||
List<ExcepcionAlarma> excepciones = const [],
|
||||
}) {
|
||||
if (!alarma.activa) return null;
|
||||
|
||||
final inicio = DateTime(
|
||||
desde.year,
|
||||
desde.month,
|
||||
desde.day,
|
||||
alarma.hora,
|
||||
alarma.minuto,
|
||||
);
|
||||
final primerCandidato =
|
||||
inicio.isAfter(desde) ? inicio : inicio.add(const Duration(days: 1));
|
||||
|
||||
return switch (alarma.tipoProgramacion) {
|
||||
TipoProgramacionAlarma.unica =>
|
||||
_esValida(alarma, primerCandidato, vacaciones, excepciones)
|
||||
? primerCandidato
|
||||
: null,
|
||||
TipoProgramacionAlarma.diaria => _buscarDiaria(
|
||||
alarma,
|
||||
primerCandidato,
|
||||
vacaciones,
|
||||
excepciones,
|
||||
),
|
||||
TipoProgramacionAlarma.diasSemana => _buscarPorDiasSemana(
|
||||
alarma,
|
||||
primerCandidato,
|
||||
vacaciones,
|
||||
excepciones,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
DateTime calcularSnooze(DateTime desde, int minutos) {
|
||||
final seguro = minutos == 3 || minutos == 5 || minutos == 10 ? minutos : 5;
|
||||
return desde.add(Duration(minutes: seguro));
|
||||
}
|
||||
|
||||
bool estaEnVacaciones(DateTime fecha, List<RangoVacaciones> vacaciones) =>
|
||||
vacaciones.any((rango) => rango.contiene(fecha));
|
||||
|
||||
DateTime? _buscarDiaria(
|
||||
AlarmaMusical alarma,
|
||||
DateTime candidato,
|
||||
List<RangoVacaciones> vacaciones,
|
||||
List<ExcepcionAlarma> excepciones,
|
||||
) {
|
||||
var actual = candidato;
|
||||
for (var i = 0; i < 370; i++) {
|
||||
if (_esValida(alarma, actual, vacaciones, excepciones)) return actual;
|
||||
actual = actual.add(const Duration(days: 1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTime? _buscarPorDiasSemana(
|
||||
AlarmaMusical alarma,
|
||||
DateTime candidato,
|
||||
List<RangoVacaciones> vacaciones,
|
||||
List<ExcepcionAlarma> excepciones,
|
||||
) {
|
||||
if (alarma.diasSemana.isEmpty) return null;
|
||||
var actual = candidato;
|
||||
for (var i = 0; i < 370; i++) {
|
||||
if (alarma.diasSemana.contains(actual.weekday) &&
|
||||
_esValida(alarma, actual, vacaciones, excepciones)) {
|
||||
return actual;
|
||||
}
|
||||
actual = actual.add(const Duration(days: 1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _esValida(
|
||||
AlarmaMusical alarma,
|
||||
DateTime candidato,
|
||||
List<RangoVacaciones> vacaciones,
|
||||
List<ExcepcionAlarma> excepciones,
|
||||
) {
|
||||
if (!alarma.sonarEnVacaciones && estaEnVacaciones(candidato, vacaciones)) {
|
||||
return false;
|
||||
}
|
||||
return !excepciones.any(
|
||||
(excepcion) =>
|
||||
excepcion.alarmaId == alarma.id &&
|
||||
_mismaEjecucion(excepcion.ejecucion, candidato),
|
||||
);
|
||||
}
|
||||
|
||||
bool _mismaEjecucion(DateTime a, DateTime b) =>
|
||||
a.year == b.year &&
|
||||
a.month == b.month &&
|
||||
a.day == b.day &&
|
||||
a.hour == b.hour &&
|
||||
a.minute == b.minute;
|
||||
}
|
||||
Reference in New Issue
Block a user