d449d8577b
- Updated Japanese, Portuguese, Russian, and Chinese localization files with new strings for search and alarm functionalities. - Enhanced the search screen with localized titles, subtitles, and filter labels. - Integrated localization into the alarm screen, including actions and messages related to alarm management. - Refactored country and language lists to use localized keys for better maintainability. - Improved user experience by providing localized hints and messages throughout the application.
223 lines
7.1 KiB
Dart
223 lines
7.1 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 '../l10n/gen/app_localizations.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> {
|
|
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;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _iniciarAlarma());
|
|
}
|
|
|
|
Future<void> _iniciarAlarma() async {
|
|
final radio = context.read<EstadoRadio>();
|
|
await _fallbackPlayer.setVolume(_volumenInicialFadeIn);
|
|
await _fallbackPlayer.setLoopMode(LoopMode.one);
|
|
|
|
final emisora = widget.alarma.emisora;
|
|
if (emisora == null) {
|
|
await _iniciarFallback();
|
|
return;
|
|
}
|
|
|
|
_radioIntentada = true;
|
|
await radio.audio.setVolumen(_volumenInicialFadeIn);
|
|
if (!widget.audioPrearrancado) {
|
|
unawaited(radio.reproducir(emisora));
|
|
}
|
|
_iniciarFadeIn();
|
|
|
|
_estadoSub = radio.estadoStream.listen((estado) {
|
|
if (estado == EstadoReproduccion.reproduciendo && mounted) {
|
|
_fallbackTimer?.cancel();
|
|
_confirmarAudioFlutterListo();
|
|
}
|
|
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();
|
|
_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;
|
|
await context.read<EstadoAlarmas>().android.confirmarAudioFlutter(
|
|
widget.alarma.id,
|
|
);
|
|
}
|
|
|
|
Future<void> _detener() async {
|
|
final radio = context.read<EstadoRadio>();
|
|
final alarmas = context.read<EstadoAlarmas>();
|
|
final navigator = Navigator.of(context);
|
|
_fallbackTimer?.cancel();
|
|
_fadeInTimer?.cancel();
|
|
await _estadoSub?.cancel();
|
|
await _fallbackPlayer.stop();
|
|
await radio.audio.pausar();
|
|
await alarmas.finalizarEjecucion(widget.alarma.id);
|
|
if (mounted) navigator.pop();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_fallbackTimer?.cancel();
|
|
_fadeInTimer?.cancel();
|
|
_estadoSub?.cancel();
|
|
_fallbackPlayer.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final alarma = widget.alarma;
|
|
final l10n = AppLocalizations.of(context);
|
|
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: Text(l10n.stopAlarmAction),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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')}';
|