feat(alarmas): agregar fade-in configurable en activacion
Build & Deploy PluriWave / Análisis de código (push) Successful in 37s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m29s

This commit is contained in:
Javier Bautista Fernández
2026-06-01 13:20:06 +02:00
parent c3a22c4658
commit de07316d79
4 changed files with 78 additions and 3 deletions
+2 -1
View File
@@ -58,6 +58,7 @@ class _PaginaPrincipal extends StatefulWidget {
}
class _PaginaPrincipalState extends State<_PaginaPrincipal> {
static const _volumenInicialFadeInAlarmas = 0.05;
int _indice = 0;
StreamSubscription<String>? _errorSubscription;
StreamSubscription<EventoAlarmaAndroid>? _alarmaSubscription;
@@ -316,7 +317,7 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
debugPrint(
'[PluriWave][alarmas] prearrancar emisora alarma id=${alarma.id} emisora=${emisora.nombre}',
);
await radio.audio.setVolumen(alarma.volumen.clamp(0.0, 1.0));
await radio.audio.setVolumen(_volumenInicialFadeInAlarmas);
unawaited(radio.reproducir(emisora));
}
+7
View File
@@ -19,6 +19,7 @@ class AlarmaMusical {
this.sonarEnVacaciones = true,
this.snoozeMinutos = 5,
this.volumen = 0.85,
this.fadeInSegundos = 0,
this.sonidoInterno = SonidoInternoAlarma.amanecer,
this.proximaEjecucion,
this.snoozeHasta,
@@ -41,6 +42,7 @@ class AlarmaMusical {
final bool sonarEnVacaciones;
final int snoozeMinutos;
final double volumen;
final int fadeInSegundos;
final SonidoInternoAlarma sonidoInterno;
final DateTime? proximaEjecucion;
final DateTime? snoozeHasta;
@@ -64,6 +66,7 @@ class AlarmaMusical {
bool? sonarEnVacaciones,
int? snoozeMinutos,
double? volumen,
int? fadeInSegundos,
SonidoInternoAlarma? sonidoInterno,
DateTime? proximaEjecucion,
bool limpiarProximaEjecucion = false,
@@ -89,6 +92,7 @@ class AlarmaMusical {
sonarEnVacaciones: sonarEnVacaciones ?? this.sonarEnVacaciones,
snoozeMinutos: snoozeMinutos ?? this.snoozeMinutos,
volumen: volumen ?? this.volumen,
fadeInSegundos: fadeInSegundos ?? this.fadeInSegundos,
sonidoInterno: sonidoInterno ?? this.sonidoInterno,
proximaEjecucion:
limpiarProximaEjecucion
@@ -122,6 +126,7 @@ class AlarmaMusical {
'sonarEnVacaciones': sonarEnVacaciones,
'snoozeMinutos': snoozeMinutos,
'volumen': volumen,
'fadeInSegundos': fadeInSegundos,
'sonidoInterno': sonidoInterno.name,
'proximaEjecucion': proximaEjecucion?.toIso8601String(),
'snoozeHasta': snoozeHasta?.toIso8601String(),
@@ -154,6 +159,8 @@ class AlarmaMusical {
sonarEnVacaciones: json['sonarEnVacaciones'] as bool? ?? true,
snoozeMinutos: json['snoozeMinutos'] as int? ?? 5,
volumen: (json['volumen'] as num?)?.toDouble() ?? 0.85,
fadeInSegundos: ((json['fadeInSegundos'] as int? ?? 0).clamp(0, 60))
as int,
sonidoInterno: _enumFromName(
SonidoInternoAlarma.values,
json['sonidoInterno'] as String?,
+42 -2
View File
@@ -25,9 +25,12 @@ class PantallaAlarmaSonando extends StatefulWidget {
}
class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
static const _volumenInicialFadeIn = 0.05;
static const _fadeStep = Duration(milliseconds: 250);
final AudioPlayer _fallbackPlayer = AudioPlayer();
StreamSubscription<EstadoReproduccion>? _estadoSub;
Timer? _fallbackTimer;
Timer? _fadeInTimer;
bool _fallbackActivo = false;
bool _radioIntentada = false;
bool _audioFlutterConfirmado = false;
@@ -40,7 +43,7 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
Future<void> _iniciarAlarma() async {
final radio = context.read<EstadoRadio>();
await _fallbackPlayer.setVolume(widget.alarma.volumen.clamp(0.0, 1.0));
await _fallbackPlayer.setVolume(_volumenInicialFadeIn);
await _fallbackPlayer.setLoopMode(LoopMode.one);
final emisora = widget.alarma.emisora;
@@ -50,10 +53,11 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
}
_radioIntentada = true;
await radio.audio.setVolumen(widget.alarma.volumen.clamp(0.0, 1.0));
await radio.audio.setVolumen(_volumenInicialFadeIn);
if (!widget.audioPrearrancado) {
unawaited(radio.reproducir(emisora));
}
_iniciarFadeIn();
_estadoSub = radio.estadoStream.listen((estado) {
if (estado == EstadoReproduccion.reproduciendo && mounted) {
@@ -78,10 +82,43 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
_fallbackActivo = true;
await _fallbackPlayer.setAsset(_assetFallback(widget.alarma.sonidoInterno));
await _fallbackPlayer.play();
_iniciarFadeIn();
await _confirmarAudioFlutterListo();
if (mounted) setState(() {});
}
void _iniciarFadeIn() {
_fadeInTimer?.cancel();
final volumenObjetivo = widget.alarma.volumen.clamp(0.0, 1.0);
final inicio = _volumenInicialFadeIn.clamp(0.0, volumenObjetivo);
final segundosFade = widget.alarma.fadeInSegundos.clamp(0, 60);
if (segundosFade <= 0 || volumenObjetivo <= inicio) {
unawaited(_aplicarVolumenGlobal(volumenObjetivo));
return;
}
final duracionTotalMs = segundosFade * 1000;
final pasos = (duracionTotalMs / _fadeStep.inMilliseconds).ceil();
var pasoActual = 0;
_fadeInTimer = Timer.periodic(_fadeStep, (timer) {
if (!mounted) {
timer.cancel();
return;
}
pasoActual++;
final t = (pasoActual / pasos).clamp(0.0, 1.0);
final volumenActual = inicio + (volumenObjetivo - inicio) * t;
unawaited(_aplicarVolumenGlobal(volumenActual));
if (t >= 1) timer.cancel();
});
}
Future<void> _aplicarVolumenGlobal(double volumen) async {
if (!mounted) return;
final radio = context.read<EstadoRadio>();
await radio.audio.setVolumen(volumen.clamp(0.0, 1.0));
await _fallbackPlayer.setVolume(volumen.clamp(0.0, 1.0));
}
Future<void> _confirmarAudioFlutterListo() async {
if (_audioFlutterConfirmado) return;
_audioFlutterConfirmado = true;
@@ -95,6 +132,7 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
final alarmas = context.read<EstadoAlarmas>();
final navigator = Navigator.of(context);
_fallbackTimer?.cancel();
_fadeInTimer?.cancel();
await _estadoSub?.cancel();
await _fallbackPlayer.stop();
await radio.audio.pausar();
@@ -107,6 +145,7 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
final alarmas = context.read<EstadoAlarmas>();
final navigator = Navigator.of(context);
_fallbackTimer?.cancel();
_fadeInTimer?.cancel();
await _estadoSub?.cancel();
await _fallbackPlayer.stop();
await radio.audio.pausar();
@@ -117,6 +156,7 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
@override
void dispose() {
_fallbackTimer?.cancel();
_fadeInTimer?.cancel();
_estadoSub?.cancel();
_fallbackPlayer.dispose();
super.dispose();
+27
View File
@@ -191,6 +191,10 @@ class _TarjetaAlarma extends StatelessWidget {
icon: Icons.volume_up_rounded,
label: '${(alarma.volumen * 100).round()}%',
),
_InfoChip(
icon: Icons.trending_up_rounded,
label: 'Fade-in ${alarma.fadeInSegundos}s',
),
],
),
const SizedBox(height: 12),
@@ -321,6 +325,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
late Set<int> _diasSemana;
late int _snooze;
late double _volumen;
late int _fadeInSegundos;
late bool _sonarEnVacaciones;
late SonidoInternoAlarma _sonidoInterno;
Emisora? _emisora;
@@ -343,6 +348,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
_diasSemana = {...alarma?.diasSemana ?? const <int>[]};
_snooze = alarma?.snoozeMinutos ?? 5;
_volumen = alarma?.volumen ?? 0.85;
_fadeInSegundos = ((alarma?.fadeInSegundos ?? 0).clamp(0, 60)) as int;
_sonarEnVacaciones = alarma?.sonarEnVacaciones ?? true;
_sonidoInterno = alarma?.sonidoInterno ?? SonidoInternoAlarma.amanecer;
_emisora = alarma?.emisora ?? context.read<EstadoRadio>().emisoraPreferida;
@@ -502,6 +508,26 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
label: '${(_volumen * 100).round()}%',
onChanged: (value) => setState(() => _volumen = value),
),
const SizedBox(height: 8),
ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Fade-in de alarma'),
subtitle: Text(
_fadeInSegundos == 0
? '0 s (sin transición)'
: '$_fadeInSegundos s (de 5% al volumen elegido)',
),
),
Slider(
value: _fadeInSegundos.toDouble(),
min: 0,
max: 60,
divisions: 60,
label: '${_fadeInSegundos}s',
onChanged:
(value) =>
setState(() => _fadeInSegundos = value.round()),
),
DropdownButtonFormField<SonidoInternoAlarma>(
initialValue: _sonidoInterno,
decoration: const InputDecoration(
@@ -655,6 +681,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
sonarEnVacaciones: _sonarEnVacaciones,
snoozeMinutos: _snooze,
volumen: _volumen,
fadeInSegundos: _fadeInSegundos.clamp(0, 60),
sonidoInterno: _sonidoInterno,
activa: true,
);