diff --git a/lib/app.dart b/lib/app.dart index 4252c6e..428bcfd 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -58,6 +58,7 @@ class _PaginaPrincipal extends StatefulWidget { } class _PaginaPrincipalState extends State<_PaginaPrincipal> { + static const _volumenInicialFadeInAlarmas = 0.05; int _indice = 0; StreamSubscription? _errorSubscription; StreamSubscription? _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)); } diff --git a/lib/modelos/alarma_musical.dart b/lib/modelos/alarma_musical.dart index ba2fb0a..c705c51 100644 --- a/lib/modelos/alarma_musical.dart +++ b/lib/modelos/alarma_musical.dart @@ -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?, diff --git a/lib/pantallas/pantalla_alarma_sonando.dart b/lib/pantallas/pantalla_alarma_sonando.dart index b5c6a32..a8f54f5 100644 --- a/lib/pantallas/pantalla_alarma_sonando.dart +++ b/lib/pantallas/pantalla_alarma_sonando.dart @@ -25,9 +25,12 @@ class PantallaAlarmaSonando extends StatefulWidget { } class _PantallaAlarmaSonandoState extends State { + static const _volumenInicialFadeIn = 0.05; + static const _fadeStep = Duration(milliseconds: 250); final AudioPlayer _fallbackPlayer = AudioPlayer(); StreamSubscription? _estadoSub; Timer? _fallbackTimer; + Timer? _fadeInTimer; bool _fallbackActivo = false; bool _radioIntentada = false; bool _audioFlutterConfirmado = false; @@ -40,7 +43,7 @@ class _PantallaAlarmaSonandoState extends State { Future _iniciarAlarma() async { final radio = context.read(); - 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 { } _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 { _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 _aplicarVolumenGlobal(double volumen) async { + if (!mounted) return; + final radio = context.read(); + await radio.audio.setVolumen(volumen.clamp(0.0, 1.0)); + await _fallbackPlayer.setVolume(volumen.clamp(0.0, 1.0)); + } + Future _confirmarAudioFlutterListo() async { if (_audioFlutterConfirmado) return; _audioFlutterConfirmado = true; @@ -95,6 +132,7 @@ class _PantallaAlarmaSonandoState extends State { final alarmas = context.read(); 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 { final alarmas = context.read(); 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 { @override void dispose() { _fallbackTimer?.cancel(); + _fadeInTimer?.cancel(); _estadoSub?.cancel(); _fallbackPlayer.dispose(); super.dispose(); diff --git a/lib/pantallas/pantalla_alarmas.dart b/lib/pantallas/pantalla_alarmas.dart index a1a33a1..31da28b 100644 --- a/lib/pantallas/pantalla_alarmas.dart +++ b/lib/pantallas/pantalla_alarmas.dart @@ -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 _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 []}; _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().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( 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, );