Adds ServicioAlarmas.registrarFalloProgramacion/limpiarFalloProgramacion, persisting a scheduling-reliability failure through the same ExcepcionAlarma model saltarProxima already uses. Only one failure record is kept per alarm (latest attempt wins) and skipNext entries for any alarm are never touched. EstadoAlarmas wiring follows next.
588 lines
20 KiB
Dart
588 lines
20 KiB
Dart
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 'persistencia_tolerante.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();
|
|
|
|
/// Current time through the injected clock, so callers that need "now"
|
|
/// (e.g. the ring screen's snooze anchor) stay testable with the same
|
|
/// fixture clock the service itself computes with.
|
|
DateTime ahora() => _reloj();
|
|
|
|
// In-memory cache + single-writer queue (Design 3.5 / S3-R7): every
|
|
// mutation runs serialized through [_enCola] and reads [_cache], killing
|
|
// the read-modify-write race the old cargar()-before-each-mutation had.
|
|
ConfiguracionAlarmas? _cache;
|
|
String? _cacheRaw;
|
|
Future<void> _cola = Future<void>.value();
|
|
|
|
// persistence-resilience (D4): set when the top-level payload could not
|
|
// be decoded at all (vs. an individual entry inside it). While true, the
|
|
// AUTOMATIC writer (recalcularTodas) must never reach disk -- only an
|
|
// explicit user mutation (which always funnels through _guardar) may
|
|
// overwrite a payload we could not read. Cleared by _guardar and by the
|
|
// next successful (even if partial) read.
|
|
bool _lecturaAlarmasDegradada = false;
|
|
|
|
Future<T> _enCola<T>(Future<T> Function() accion) {
|
|
final resultado = _cola.then((_) => accion());
|
|
_cola = resultado.then((_) {}, onError: (_) {});
|
|
return resultado;
|
|
}
|
|
|
|
/// Re-reads from persistent storage (refreshing the cache) so writes done
|
|
/// outside this service — e.g. a settings import that rewrites the raw
|
|
/// key — are always picked up.
|
|
Future<ConfiguracionAlarmas> cargar() => _enCola(() {
|
|
_cache = null;
|
|
_cacheRaw = null;
|
|
return _configActual();
|
|
});
|
|
|
|
Future<ConfiguracionAlarmas> _configActual() async {
|
|
final existente = _cache;
|
|
if (existente != null) return existente;
|
|
final prefs = await _resolverPrefs();
|
|
final raw = prefs.getString(_keyConfig);
|
|
final config = _parsear(raw);
|
|
_cache = config;
|
|
return config;
|
|
}
|
|
|
|
/// Parses the persisted [raw] payload with per-entry tolerance
|
|
/// (persistence-resilience D1/D2) and updates [_cacheRaw] /
|
|
/// [_lecturaAlarmasDegradada] as a side effect, since each outcome needs
|
|
/// a DIFFERENT cached-raw value:
|
|
/// - top-level decode failure (bad JSON, or any container-level field of
|
|
/// the wrong shape) -> DEGRADED: empty config, [_cacheRaw] keeps the
|
|
/// corrupt [raw] untouched, [_lecturaAlarmasDegradada] set so
|
|
/// `recalcularTodas` never overwrites it (D4).
|
|
/// - decodes, but one or more entries are malformed -> PARTIAL: only the
|
|
/// survivors are kept, [_cacheRaw] is normalized to their own
|
|
/// serialization -- NOT the corrupt raw (D3) -- so the automatic
|
|
/// writer's dirty-guard compares against a coherent baseline instead of
|
|
/// re-firing on every 60s tick.
|
|
/// - fully healthy payload -> unchanged behavior; clears the flag if a
|
|
/// previous read had set it (suppression lifts on next successful
|
|
/// read).
|
|
ConfiguracionAlarmas _parsear(String? raw) {
|
|
if (raw == null || raw.trim().isEmpty) {
|
|
_lecturaAlarmasDegradada = false;
|
|
_cacheRaw = raw;
|
|
return const ConfiguracionAlarmas(
|
|
alarmas: [],
|
|
vacaciones: [],
|
|
excepciones: [],
|
|
);
|
|
}
|
|
try {
|
|
final data = jsonDecode(raw) as Map<String, dynamic>;
|
|
final alarmas = parseListaTolerante<AlarmaMusical>(
|
|
data['alarmas'] as List?,
|
|
AlarmaMusical.fromJson,
|
|
subsistema: 'alarmas',
|
|
coleccion: 'alarmas',
|
|
);
|
|
final vacaciones = parseListaTolerante<RangoVacaciones>(
|
|
data['vacaciones'] as List?,
|
|
RangoVacaciones.fromJson,
|
|
subsistema: 'alarmas',
|
|
coleccion: 'vacaciones',
|
|
);
|
|
final excepciones = parseListaTolerante<ExcepcionAlarma>(
|
|
data['excepciones'] as List?,
|
|
ExcepcionAlarma.fromJson,
|
|
subsistema: 'alarmas',
|
|
coleccion: 'excepciones',
|
|
);
|
|
final config = ConfiguracionAlarmas(
|
|
alarmas: alarmas.validas,
|
|
vacaciones: vacaciones.validas,
|
|
excepciones: excepciones.validas,
|
|
);
|
|
_lecturaAlarmasDegradada = false;
|
|
final huboSaltos =
|
|
alarmas.saltadas > 0 ||
|
|
vacaciones.saltadas > 0 ||
|
|
excepciones.saltadas > 0;
|
|
_cacheRaw = huboSaltos ? _serializar(config) : raw;
|
|
return config;
|
|
} catch (e) {
|
|
_lecturaAlarmasDegradada = true;
|
|
_cacheRaw = raw;
|
|
registrarSaltoPersistencia(
|
|
subsistema: 'alarmas',
|
|
detalle: _keyConfig,
|
|
razon: e.toString(),
|
|
);
|
|
return const ConfiguracionAlarmas(
|
|
alarmas: [],
|
|
vacaciones: [],
|
|
excepciones: [],
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<ConfiguracionAlarmas> guardarAlarma(
|
|
AlarmaMusical alarma, {
|
|
List<RangoVacaciones>? vacaciones,
|
|
List<ExcepcionAlarma>? excepciones,
|
|
}) => _enCola(() async {
|
|
final config = await _configActual();
|
|
final ahora = _reloj();
|
|
final vacacionesActuales = vacaciones ?? config.vacaciones;
|
|
final excepcionesActuales = excepciones ?? config.excepciones;
|
|
final alarmas = List<AlarmaMusical>.from(config.alarmas);
|
|
final index = alarmas.indexWhere((a) => a.id == alarma.id);
|
|
final actualizada = alarma.copyWith(
|
|
creadaEn: alarma.creadaEn ?? ahora,
|
|
actualizadaEn: ahora,
|
|
);
|
|
if (index >= 0) {
|
|
alarmas[index] = actualizada;
|
|
} else {
|
|
alarmas.add(actualizada);
|
|
}
|
|
// Recalculate every alarm, not just the one just saved: siblings can
|
|
// hold a proximaEjecucion snapshot from the last periodic tick that is
|
|
// stale (even past-due), which would otherwise outrank a freshly
|
|
// created/activated/edited alarm when EstadoAlarmas.proximaAlarma sorts
|
|
// by proximaProgramable.
|
|
final nuevo = ConfiguracionAlarmas(
|
|
alarmas: _recalcularLista(
|
|
alarmas,
|
|
vacacionesActuales,
|
|
excepcionesActuales,
|
|
),
|
|
vacaciones: vacacionesActuales,
|
|
excepciones: excepcionesActuales,
|
|
);
|
|
await _guardar(nuevo);
|
|
return nuevo;
|
|
});
|
|
|
|
Future<ConfiguracionAlarmas> eliminarAlarma(String id) => _enCola(() async {
|
|
final config = await _configActual();
|
|
final restantes = config.alarmas.where((a) => a.id != id).toList();
|
|
final excepciones =
|
|
config.excepciones.where((e) => e.alarmaId != id).toList();
|
|
final nuevo = ConfiguracionAlarmas(
|
|
alarmas: _recalcularLista(restantes, config.vacaciones, excepciones),
|
|
vacaciones: config.vacaciones,
|
|
excepciones: excepciones,
|
|
);
|
|
await _guardar(nuevo);
|
|
return nuevo;
|
|
});
|
|
|
|
Future<ConfiguracionAlarmas> guardarVacaciones(
|
|
List<RangoVacaciones> vacaciones,
|
|
) => _enCola(() async {
|
|
final config = await _configActual();
|
|
final normalizadas =
|
|
vacaciones.map((v) => v.normalizado()).toList()
|
|
..sort((a, b) => a.inicioDia.compareTo(b.inicioDia));
|
|
final nuevo = ConfiguracionAlarmas(
|
|
alarmas: _recalcularLista(
|
|
config.alarmas,
|
|
normalizadas,
|
|
config.excepciones,
|
|
),
|
|
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() => _enCola(() async {
|
|
final config = await _configActual();
|
|
// persistence-resilience (D4): a degraded top-level read must never
|
|
// let this AUTOMATIC writer reach disk -- only an explicit user
|
|
// mutation (guardarAlarma/eliminarAlarma/etc., via _guardar) may
|
|
// persist over a payload we could not read.
|
|
if (_lecturaAlarmasDegradada) return config;
|
|
final alarmas = _recalcularLista(
|
|
config.alarmas,
|
|
config.vacaciones,
|
|
config.excepciones,
|
|
);
|
|
final nuevo = ConfiguracionAlarmas(
|
|
alarmas: alarmas,
|
|
vacaciones: config.vacaciones,
|
|
excepciones: config.excepciones,
|
|
);
|
|
// Dirty-guard (S3-R5): this runs every minute from the refresh timer;
|
|
// skip the SharedPreferences write when nothing actually changed.
|
|
final nuevoRaw = _serializar(nuevo);
|
|
final actualRaw = _cacheRaw ?? _serializar(config);
|
|
if (nuevoRaw == actualRaw) return config;
|
|
await _guardar(nuevo, raw: nuevoRaw);
|
|
return nuevo;
|
|
});
|
|
|
|
Future<ConfiguracionAlarmas> sincronizarEjecucionesNativas(
|
|
Map<String, DateTime> ejecuciones,
|
|
) {
|
|
if (ejecuciones.isEmpty) return cargar();
|
|
return _enCola(() => _sincronizarEjecucionesNativasInterno(ejecuciones));
|
|
}
|
|
|
|
Future<ConfiguracionAlarmas> _sincronizarEjecucionesNativasInterno(
|
|
Map<String, DateTime> ejecuciones,
|
|
) async {
|
|
final config = await _configActual();
|
|
final ahora = _reloj();
|
|
var huboCambios = false;
|
|
final alarmas =
|
|
config.alarmas.map((alarma) {
|
|
final gestionadaEn = ejecuciones[alarma.id];
|
|
if (gestionadaEn == null) return alarma;
|
|
final ultima = alarma.ultimaEjecucionGestionada;
|
|
if (ultima != null && !gestionadaEn.isAfter(ultima)) return alarma;
|
|
|
|
final proxima = alarma.proximaProgramable;
|
|
if (proxima != null &&
|
|
proxima.isAfter(
|
|
gestionadaEn.add(
|
|
ServicioProgramacionAlarmas.toleranciaDisparoInminente,
|
|
),
|
|
)) {
|
|
return alarma;
|
|
}
|
|
|
|
final siguiente = _programacion.calcularSiguienteDespuesDeEjecucion(
|
|
alarma: alarma,
|
|
ejecucion: gestionadaEn,
|
|
vacaciones: config.vacaciones,
|
|
excepciones: config.excepciones,
|
|
);
|
|
huboCambios = true;
|
|
return alarma.copyWith(
|
|
activa:
|
|
alarma.tipoProgramacion == TipoProgramacionAlarma.unica
|
|
? false
|
|
: alarma.activa,
|
|
proximaEjecucion: siguiente,
|
|
limpiarProximaEjecucion: true,
|
|
limpiarSnooze: true,
|
|
ultimaEjecucionGestionada: gestionadaEn,
|
|
actualizadaEn: ahora,
|
|
);
|
|
}).toList();
|
|
|
|
if (!huboCambios) return config;
|
|
final nuevo = ConfiguracionAlarmas(
|
|
alarmas: _recalcularLista(alarmas, config.vacaciones, config.excepciones),
|
|
vacaciones: config.vacaciones,
|
|
excepciones: config.excepciones,
|
|
);
|
|
await _guardar(nuevo);
|
|
return nuevo;
|
|
}
|
|
|
|
Future<ConfiguracionAlarmas> saltarProxima(
|
|
String alarmaId,
|
|
) => _enCola(() async {
|
|
final config = await _configActual();
|
|
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 nuevo = ConfiguracionAlarmas(
|
|
alarmas: _recalcularLista(config.alarmas, config.vacaciones, excepciones),
|
|
vacaciones: config.vacaciones,
|
|
excepciones: excepciones,
|
|
);
|
|
await _guardar(nuevo);
|
|
return nuevo;
|
|
});
|
|
|
|
/// Records a scheduling-reliability failure for [alarmaId] (fix/alarmas-
|
|
/// fallos-silenciosos): reuses the SAME `ExcepcionAlarma` model
|
|
/// `saltarProxima` already persists, so `EstadoAlarmas.ultimaExcepcionPara`
|
|
/// surfaces it on the exact alarm card affected instead of only a
|
|
/// transient, alarm-agnostic message. A previous FAILURE record for the
|
|
/// same alarm is replaced (only the latest attempt's outcome matters) --
|
|
/// any `skipNext` exception for this or other alarms is left untouched.
|
|
/// Never affects scheduling: `ServicioProgramacionAlarmas._esValida` only
|
|
/// treats `tipoSaltoSiguiente` as an actual skip.
|
|
Future<ConfiguracionAlarmas> registrarFalloProgramacion(
|
|
String alarmaId,
|
|
DateTime ejecucion,
|
|
String tipo,
|
|
) => _enCola(() async {
|
|
final config = await _configActual();
|
|
final excepciones = [
|
|
..._sinFalloPrevio(config.excepciones, alarmaId),
|
|
ExcepcionAlarma(alarmaId: alarmaId, ejecucion: ejecucion, tipo: tipo),
|
|
];
|
|
final nuevo = ConfiguracionAlarmas(
|
|
alarmas: config.alarmas,
|
|
vacaciones: config.vacaciones,
|
|
excepciones: excepciones,
|
|
);
|
|
await _guardar(nuevo);
|
|
return nuevo;
|
|
});
|
|
|
|
/// Clears any outstanding failure record for [alarmaId] (a subsequent
|
|
/// scheduling attempt succeeded). No-op when there is nothing to clear.
|
|
Future<ConfiguracionAlarmas> limpiarFalloProgramacion(
|
|
String alarmaId,
|
|
) => _enCola(() async {
|
|
final config = await _configActual();
|
|
final sinFallo = _sinFalloPrevio(config.excepciones, alarmaId);
|
|
if (sinFallo.length == config.excepciones.length) return config;
|
|
final nuevo = ConfiguracionAlarmas(
|
|
alarmas: config.alarmas,
|
|
vacaciones: config.vacaciones,
|
|
excepciones: sinFallo,
|
|
);
|
|
await _guardar(nuevo);
|
|
return nuevo;
|
|
});
|
|
|
|
List<ExcepcionAlarma> _sinFalloPrevio(
|
|
List<ExcepcionAlarma> excepciones,
|
|
String alarmaId,
|
|
) => excepciones
|
|
.where(
|
|
(e) =>
|
|
!(e.alarmaId == alarmaId &&
|
|
ExcepcionAlarma.tiposFallo.contains(e.tipo)),
|
|
)
|
|
.toList();
|
|
|
|
Future<ConfiguracionAlarmas> posponerEjecucion(
|
|
String alarmaId,
|
|
DateTime ejecucion,
|
|
int minutos,
|
|
) async {
|
|
// Unified snooze anchor (Design 2.2): occurrence + minutes, clamped to
|
|
// now + minutes when the target already passed. Matches the native
|
|
// AlarmScheduler.snooze/postponeNext semantics so both layers always
|
|
// land on the same re-fire time.
|
|
final seguros = minutos.clamp(1, 120);
|
|
final objetivo = ejecucion.add(Duration(minutes: seguros));
|
|
final ahora = _reloj();
|
|
final snoozeHasta =
|
|
objetivo.isAfter(ahora)
|
|
? objetivo
|
|
: ahora.add(Duration(minutes: seguros));
|
|
return posponerEjecucionHasta(alarmaId, ejecucion, snoozeHasta);
|
|
}
|
|
|
|
Future<ConfiguracionAlarmas> posponerEjecucionHasta(
|
|
String alarmaId,
|
|
DateTime ejecucion,
|
|
DateTime snoozeHasta,
|
|
) => _enCola(() async {
|
|
final config = await _configActual();
|
|
final ahora = _reloj();
|
|
final alarmas =
|
|
config.alarmas
|
|
.map(
|
|
(a) =>
|
|
a.id == alarmaId
|
|
? a.copyWith(
|
|
snoozeHasta: snoozeHasta,
|
|
snoozeOrigen: ejecucion,
|
|
ultimaEjecucionGestionada: ejecucion,
|
|
actualizadaEn: ahora,
|
|
)
|
|
: a,
|
|
)
|
|
.toList();
|
|
final nuevo = ConfiguracionAlarmas(
|
|
alarmas: _recalcularLista(alarmas, config.vacaciones, config.excepciones),
|
|
vacaciones: config.vacaciones,
|
|
excepciones: config.excepciones,
|
|
);
|
|
await _guardar(nuevo);
|
|
return nuevo;
|
|
});
|
|
|
|
Future<ConfiguracionAlarmas> completarEjecucion(
|
|
String alarmaId,
|
|
DateTime ejecucion,
|
|
) => _enCola(() async {
|
|
final config = await _configActual();
|
|
final ahora = _reloj();
|
|
final alarmas =
|
|
config.alarmas.map((a) {
|
|
if (a.id != alarmaId) return a;
|
|
final siguiente = _programacion.calcularSiguienteDespuesDeEjecucion(
|
|
alarma: a,
|
|
ejecucion: ejecucion,
|
|
vacaciones: config.vacaciones,
|
|
excepciones: config.excepciones,
|
|
);
|
|
return a.copyWith(
|
|
activa:
|
|
a.tipoProgramacion == TipoProgramacionAlarma.unica
|
|
? false
|
|
: a.activa,
|
|
proximaEjecucion: siguiente,
|
|
limpiarProximaEjecucion: true,
|
|
limpiarSnooze: true,
|
|
ultimaEjecucionGestionada: ejecucion,
|
|
actualizadaEn: ahora,
|
|
);
|
|
}).toList();
|
|
final nuevo = ConfiguracionAlarmas(
|
|
alarmas: _recalcularLista(alarmas, config.vacaciones, config.excepciones),
|
|
vacaciones: config.vacaciones,
|
|
excepciones: config.excepciones,
|
|
);
|
|
await _guardar(nuevo);
|
|
return nuevo;
|
|
});
|
|
|
|
AlarmaMusical crearAlarma({
|
|
required String nombre,
|
|
required int hora,
|
|
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(
|
|
id: _uuid.v4(),
|
|
nombre: nombre,
|
|
hora: hora,
|
|
minuto: minuto,
|
|
tipoProgramacion: tipoProgramacion,
|
|
diasSemana: diasSemana,
|
|
fechaUnica: fechaUnica,
|
|
emisora: emisora,
|
|
emisoraFallback: emisoraFallback,
|
|
sonarEnVacaciones: sonarEnVacaciones,
|
|
snoozeMinutos: snoozeMinutos,
|
|
volumen: volumen,
|
|
sonidoInterno: sonidoInterno,
|
|
creadaEn: ahora,
|
|
actualizadaEn: ahora,
|
|
);
|
|
}
|
|
|
|
/// Persists [config] and refreshes the in-memory cache (the cache is
|
|
/// "invalidated" by replacing it with the just-written state).
|
|
Future<void> _guardar(ConfiguracionAlarmas config, {String? raw}) async {
|
|
final serializado = raw ?? _serializar(config);
|
|
final prefs = await _resolverPrefs();
|
|
await prefs.setString(_keyConfig, serializado);
|
|
_cache = config;
|
|
_cacheRaw = serializado;
|
|
// persistence-resilience (D4): every explicit mutation funnels through
|
|
// here, so this is where write authority is restored after a degraded
|
|
// read -- user intent wins over a suppressed automatic writer.
|
|
_lecturaAlarmasDegradada = false;
|
|
}
|
|
|
|
String _serializar(ConfiguracionAlarmas config) => 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,
|
|
) {
|
|
final ahora = _reloj();
|
|
// S2-R5: a disabled alarm must not keep a pending snooze; clearing it
|
|
// here guarantees the snoozed occurrence dies with the alarm.
|
|
final snoozeActivo =
|
|
alarma.activa &&
|
|
alarma.snoozeHasta != null &&
|
|
alarma.snoozeHasta!.isAfter(ahora);
|
|
final proxima = _programacion.calcularProxima(
|
|
alarma: alarma,
|
|
desde: ahora,
|
|
vacaciones: vacaciones,
|
|
excepciones: excepciones,
|
|
);
|
|
return alarma.copyWith(
|
|
proximaEjecucion: proxima,
|
|
limpiarProximaEjecucion: true,
|
|
limpiarSnooze: !snoozeActivo,
|
|
);
|
|
}
|
|
|
|
// Every mutation that can shift "which alarm fires next" (create, edit,
|
|
// activate/deactivate, delete, fire, snooze, skip) recalculates the whole
|
|
// list, not just the alarm it touched directly. Siblings otherwise keep a
|
|
// proximaEjecucion snapshot from the last periodic tick, which can be
|
|
// stale (or already past-due) and wrongly outrank a freshly updated alarm
|
|
// wherever proximaProgramable values are compared (EstadoAlarmas.proximaAlarma).
|
|
List<AlarmaMusical> _recalcularLista(
|
|
List<AlarmaMusical> alarmas,
|
|
List<RangoVacaciones> vacaciones,
|
|
List<ExcepcionAlarma> excepciones,
|
|
) => alarmas.map((a) => _recalcular(a, vacaciones, excepciones)).toList();
|
|
|
|
Future<SharedPreferences> _resolverPrefs() async =>
|
|
_prefs ?? SharedPreferences.getInstance();
|
|
}
|