feat(alarms): add native ringing service
Build & Deploy Pluriwave / Análisis de código (push) Successful in 26s
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 2m8s

This commit is contained in:
2026-05-22 20:02:09 +02:00
parent c8fff0d977
commit 3ab138a4fa
8 changed files with 437 additions and 90 deletions
+90 -88
View File
@@ -214,9 +214,9 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).skipCurrentAlarmExecution(
alarma.nombre,
),
AppLocalizations.of(
context,
).skipCurrentAlarmExecution(alarma.nombre),
),
),
);
@@ -249,6 +249,7 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
_alarmaSonandoId = alarma.id;
try {
await alarmas.android.detenerSonidoNativo(alarma.id);
await _prearrancarAudioAlarma(alarma);
if (!mounted) return;
await Navigator.of(context).push(
@@ -286,89 +287,96 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
showDragHandle: true,
builder:
(ctx) => Consumer<EstadoRadio>(
builder: (ctx, estado, _) => SafeArea(
child: Padding(
padding: PluriLayout.sheetPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(ctx).sleepTimer,
style: Theme.of(ctx).textTheme.titleLarge,
),
const SizedBox(height: PluriLayout.sectionGap),
Text(
AppLocalizations.of(ctx).sleepTimerDescription,
style: Theme.of(ctx).textTheme.bodySmall,
),
const SizedBox(height: PluriLayout.panelGap),
if (estado.timer.activo)
StreamBuilder<Duration>(
stream: estado.timer.tiempoRestanteStream,
builder: (ctx, snap) {
final restante =
snap.data ?? estado.timer.tiempoRestante;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
builder:
(ctx, estado, _) => SafeArea(
child: Padding(
padding: PluriLayout.sheetPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(ctx).sleepTimer,
style: Theme.of(ctx).textTheme.titleLarge,
),
const SizedBox(height: PluriLayout.sectionGap),
Text(
AppLocalizations.of(ctx).sleepTimerDescription,
style: Theme.of(ctx).textTheme.bodySmall,
),
const SizedBox(height: PluriLayout.panelGap),
if (estado.timer.activo)
StreamBuilder<Duration>(
stream: estado.timer.tiempoRestanteStream,
builder: (ctx, snap) {
final restante =
snap.data ?? estado.timer.tiempoRestante;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
_formatearDuracionTimer(restante),
style:
Theme.of(ctx).textTheme.headlineMedium,
),
const SizedBox(
height: PluriLayout.compactGap,
),
FilledButton.tonal(
onPressed: () {
estado.cancelarTimer();
Navigator.pop(ctx);
},
child: Text(
AppLocalizations.of(ctx).cancelTimer,
),
),
],
);
},
)
else
Wrap(
spacing: PluriLayout.compactGap,
runSpacing: PluriLayout.compactGap,
children: [
Text(
_formatearDuracionTimer(restante),
style: Theme.of(ctx).textTheme.headlineMedium,
),
const SizedBox(height: PluriLayout.compactGap),
FilledButton.tonal(
onPressed: () {
estado.cancelarTimer();
for (final segundos
in estado.timerSuenoPresetsSegundos)
ActionChip(
label: Text(
_formatearDuracionTimer(
Duration(seconds: segundos),
),
),
onPressed: () {
estado.iniciarTimerDuracion(
Duration(seconds: segundos),
);
Navigator.pop(ctx);
},
),
ActionChip(
avatar: const Icon(
Icons.tune_rounded,
size: 18,
),
label: Text(
AppLocalizations.of(ctx).optionOther,
),
onPressed: () async {
final duracion =
await _pedirDuracionPersonalizada(ctx);
if (duracion == null || !ctx.mounted) return;
estado.iniciarTimerDuracion(duracion);
Navigator.pop(ctx);
},
child: Text(
AppLocalizations.of(ctx).cancelTimer,
),
),
],
);
},
)
else
Wrap(
spacing: PluriLayout.compactGap,
runSpacing: PluriLayout.compactGap,
children: [
for (final segundos
in estado.timerSuenoPresetsSegundos)
ActionChip(
label: Text(
_formatearDuracionTimer(
Duration(seconds: segundos),
),
),
onPressed: () {
estado.iniciarTimerDuracion(
Duration(seconds: segundos),
);
Navigator.pop(ctx);
},
),
ActionChip(
avatar: const Icon(Icons.tune_rounded, size: 18),
label: Text(
AppLocalizations.of(ctx).optionOther,
),
onPressed: () async {
final duracion =
await _pedirDuracionPersonalizada(ctx);
if (duracion == null || !ctx.mounted) return;
estado.iniciarTimerDuracion(duracion);
Navigator.pop(ctx);
},
),
],
),
],
],
),
),
),
),
),
),
);
}
@@ -429,9 +437,7 @@ class _TimerPersonalizadoSheetState extends State<_TimerPersonalizadoSheet> {
if (duracion <= Duration.zero) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).durationGreaterThanZero,
),
content: Text(AppLocalizations.of(context).durationGreaterThanZero),
),
);
return;
@@ -484,18 +490,14 @@ class _TimerPersonalizadoSheetState extends State<_TimerPersonalizadoSheet> {
const SizedBox(height: PluriLayout.compactGap),
SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
title: Text(
AppLocalizations.of(context).saveQuickAccess,
),
title: Text(AppLocalizations.of(context).saveQuickAccess),
value: _guardarPreset,
onChanged: (value) => setState(() => _guardarPreset = value),
),
const SizedBox(height: PluriLayout.sectionGap),
FilledButton.icon(
icon: const Icon(Icons.bedtime_rounded),
label: Text(
AppLocalizations.of(context).startTimer,
),
label: Text(AppLocalizations.of(context).startTimer),
onPressed: _confirmar,
),
],
@@ -80,6 +80,10 @@ class ServicioAlarmasAndroid {
'triggerAtMillis': proxima.millisecondsSinceEpoch,
'preNoticeAtMillis':
proxima.subtract(const Duration(minutes: 30)).millisecondsSinceEpoch,
'stationName': alarma.emisora?.nombre,
'stationUrl': alarma.emisora?.url,
'fallbackSound': alarma.sonidoInterno.name,
'volume': alarma.volumen,
});
}
@@ -89,6 +93,9 @@ class ServicioAlarmasAndroid {
Future<void> ocultarNotificacionAlarma(String alarmaId) =>
_logAndInvokeVoid('dismissAlarmNotification', {'id': alarmaId});
Future<void> detenerSonidoNativo(String alarmaId) =>
_logAndInvokeVoid('stopNativeAlarmSound', {'id': alarmaId});
Future<DiagnosticoAlarmasAndroid> diagnostico() async {
debugPrint('[PluriWave][alarmas] diagnostico android');
final raw = await _channel.invokeMethod<Map<Object?, Object?>>(