Files
pluriwave/lib/pantallas/pantalla_alarma_sonando.dart
T
FreeTLab cfea818133
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 2m0s
Build & Deploy Pluriwave / Análisis de código (push) Successful in 24s
fix(alarms): prevent overlapping playback
2026-05-22 19:40:09 +02:00

195 lines
6.2 KiB
Dart

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,
this.audioPrearrancado = false,
});
final AlarmaMusical alarma;
final bool audioPrearrancado;
@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));
if (!widget.audioPrearrancado) {
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();
});
if (widget.audioPrearrancado && radio.audio.estaSonando) {
_fallbackTimer?.cancel();
}
}
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')}';