fix(alarm): improve firing and preferred station
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,15 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
NotificationManagerCompat.from(context).cancel(
|
NotificationManagerCompat.from(context).cancel(
|
||||||
PluriWaveAlarmReceiver.notificationIdForAlarm(id)
|
PluriWaveAlarmReceiver.notificationIdForAlarm(id)
|
||||||
)
|
)
|
||||||
|
NotificationManagerCompat.from(context).cancel(
|
||||||
|
PluriWaveAlarmReceiver.fireNotificationIdForAlarm(id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissFireNotification(id: String) {
|
||||||
|
NotificationManagerCompat.from(context).cancel(
|
||||||
|
PluriWaveAlarmReceiver.fireNotificationIdForAlarm(id)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canScheduleExactAlarms(): Boolean {
|
fun canScheduleExactAlarms(): Boolean {
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ class MainActivity : AudioServiceActivity() {
|
|||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"dismissAlarmNotification" -> {
|
||||||
|
val id = call.argument<String>("id")
|
||||||
|
if (id == null) {
|
||||||
|
result.error("INVALID_ALARM", "Missing alarm id", null)
|
||||||
|
} else {
|
||||||
|
alarmScheduler.dismissFireNotification(id)
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
"diagnostics" -> {
|
"diagnostics" -> {
|
||||||
result.success(
|
result.success(
|
||||||
mapOf(
|
mapOf(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
putExtra(EXTRA_ALARM_TITLE, title)
|
putExtra(EXTRA_ALARM_TITLE, title)
|
||||||
putExtra(EXTRA_ALARM_ACTION, ACTION_FIRE)
|
putExtra(EXTRA_ALARM_ACTION, ACTION_FIRE)
|
||||||
}
|
}
|
||||||
|
showFireNotification(context, alarmId, title, launch)
|
||||||
context.startActivity(launch)
|
context.startActivity(launch)
|
||||||
}
|
}
|
||||||
ACTION_PRE_NOTICE -> {
|
ACTION_PRE_NOTICE -> {
|
||||||
@@ -41,6 +42,40 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showFireNotification(
|
||||||
|
context: Context,
|
||||||
|
alarmId: String,
|
||||||
|
title: String,
|
||||||
|
launch: Intent
|
||||||
|
) {
|
||||||
|
ensureFireChannel(context)
|
||||||
|
val fullScreenIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
requestCode(alarmId, 10),
|
||||||
|
launch,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
val notification = NotificationCompat.Builder(context, FIRE_CHANNEL_ID)
|
||||||
|
.setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
|
||||||
|
.setContentTitle("Alarma PluriWave")
|
||||||
|
.setContentText(title)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setAutoCancel(false)
|
||||||
|
.setContentIntent(fullScreenIntent)
|
||||||
|
.setFullScreenIntent(fullScreenIntent, true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
try {
|
||||||
|
NotificationManagerCompat.from(context).notify(
|
||||||
|
fireNotificationIdForAlarm(alarmId),
|
||||||
|
notification,
|
||||||
|
)
|
||||||
|
} catch (_: SecurityException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showPreNoticeNotification(context: Context, alarmId: String, title: String) {
|
private fun showPreNoticeNotification(context: Context, alarmId: String, title: String) {
|
||||||
ensureChannel(context)
|
ensureChannel(context)
|
||||||
|
|
||||||
@@ -82,6 +117,23 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
NotificationManagerCompat.from(context).notify(notificationIdForAlarm(alarmId), notification)
|
NotificationManagerCompat.from(context).notify(notificationIdForAlarm(alarmId), notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ensureFireChannel(context: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
val existing = manager.getNotificationChannel(FIRE_CHANNEL_ID)
|
||||||
|
if (existing != null) return
|
||||||
|
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
FIRE_CHANNEL_ID,
|
||||||
|
"Alarmas sonando",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
).apply {
|
||||||
|
description = "Pantalla urgente cuando una alarma musical debe sonar"
|
||||||
|
enableVibration(true)
|
||||||
|
}
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
private fun ensureChannel(context: Context) {
|
private fun ensureChannel(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
@@ -104,6 +156,7 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CHANNEL_ID = "pluriwave_alarm_pre_notice"
|
const val CHANNEL_ID = "pluriwave_alarm_pre_notice"
|
||||||
|
const val FIRE_CHANNEL_ID = "pluriwave_alarm_fire"
|
||||||
const val ACTION_FIRE = "es.freetimelab.pluriwave.alarm.FIRE"
|
const val ACTION_FIRE = "es.freetimelab.pluriwave.alarm.FIRE"
|
||||||
const val ACTION_PRE_NOTICE = "es.freetimelab.pluriwave.alarm.PRE_NOTICE"
|
const val ACTION_PRE_NOTICE = "es.freetimelab.pluriwave.alarm.PRE_NOTICE"
|
||||||
const val ACTION_SKIP_NEXT = "es.freetimelab.pluriwave.alarm.SKIP_NEXT"
|
const val ACTION_SKIP_NEXT = "es.freetimelab.pluriwave.alarm.SKIP_NEXT"
|
||||||
@@ -112,5 +165,6 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
const val EXTRA_ALARM_ACTION = "alarmAction"
|
const val EXTRA_ALARM_ACTION = "alarmAction"
|
||||||
|
|
||||||
fun notificationIdForAlarm(alarmId: String): Int = 53 * alarmId.hashCode() + 7
|
fun notificationIdForAlarm(alarmId: String): Int = 53 * alarmId.hashCode() + 7
|
||||||
|
fun fireNotificationIdForAlarm(alarmId: String): Int = 59 * alarmId.hashCode() + 9
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -158,8 +158,9 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
|||||||
body: SafeArea(top: false, child: _paginas[_indice]),
|
body: SafeArea(top: false, child: _paginas[_indice]),
|
||||||
bottomNavigationBar: SafeArea(
|
bottomNavigationBar: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
|
minimum: const EdgeInsets.only(bottom: 8),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 10),
|
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -64,7 +64,12 @@ class EstadoAlarmas extends ChangeNotifier {
|
|||||||
Future<void> guardarAlarma(AlarmaMusical alarma) async {
|
Future<void> guardarAlarma(AlarmaMusical alarma) async {
|
||||||
final config = await servicio.guardarAlarma(alarma);
|
final config = await servicio.guardarAlarma(alarma);
|
||||||
_aplicar(config);
|
_aplicar(config);
|
||||||
await android.programar(_alarmas.firstWhere((a) => a.id == alarma.id));
|
try {
|
||||||
|
await android.programar(_alarmas.firstWhere((a) => a.id == alarma.id));
|
||||||
|
} catch (e) {
|
||||||
|
_error =
|
||||||
|
'Alarma guardada, pero Android no pudo programarla todavía: $e';
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,10 +116,12 @@ class EstadoAlarmas extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> posponerAlarma(AlarmaMusical alarma, int minutos) async {
|
Future<void> posponerAlarma(AlarmaMusical alarma, int minutos) async {
|
||||||
final proxima = DateTime.now().add(Duration(minutes: minutos));
|
final proxima = DateTime.now().add(Duration(minutes: minutos));
|
||||||
|
await android.ocultarNotificacionAlarma(alarma.id);
|
||||||
await android.programar(alarma.copyWith(proximaEjecucion: proxima));
|
await android.programar(alarma.copyWith(proximaEjecucion: proxima));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> finalizarEjecucion(String alarmaId) async {
|
Future<void> finalizarEjecucion(String alarmaId) async {
|
||||||
|
await android.ocultarNotificacionAlarma(alarmaId);
|
||||||
await refrescarProgramacion();
|
await refrescarProgramacion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:geocoding/geocoding.dart';
|
import 'package:geocoding/geocoding.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../modelos/emisora.dart';
|
import '../modelos/emisora.dart';
|
||||||
import '../modelos/preset_ecualizador.dart';
|
import '../modelos/preset_ecualizador.dart';
|
||||||
@@ -53,6 +54,7 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
Future<void>? _initFuture;
|
Future<void>? _initFuture;
|
||||||
int _revisionReproduccion = 0;
|
int _revisionReproduccion = 0;
|
||||||
Emisora? _emisoraSeleccionada;
|
Emisora? _emisoraSeleccionada;
|
||||||
|
String? _emisoraPreferidaUuid;
|
||||||
|
|
||||||
// Errores de reproducción → SnackBar.
|
// Errores de reproducción → SnackBar.
|
||||||
final _errorController = StreamController<String>.broadcast();
|
final _errorController = StreamController<String>.broadcast();
|
||||||
@@ -84,6 +86,7 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
String? _ultimoIdiomaBusqueda;
|
String? _ultimoIdiomaBusqueda;
|
||||||
String? _ultimoTagBusqueda;
|
String? _ultimoTagBusqueda;
|
||||||
String? _errorCarga;
|
String? _errorCarga;
|
||||||
|
static const _keyEmisoraPreferida = 'emisora_preferida_uuid_v1';
|
||||||
|
|
||||||
List<Emisora> get populares => _populares;
|
List<Emisora> get populares => _populares;
|
||||||
List<Emisora> get tendencias => _tendencias;
|
List<Emisora> get tendencias => _tendencias;
|
||||||
@@ -100,6 +103,8 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
String? get errorCercanas => _errorCercanas;
|
String? get errorCercanas => _errorCercanas;
|
||||||
String? get error => _errorCarga;
|
String? get error => _errorCarga;
|
||||||
Emisora? get emisoraActual => _emisoraSeleccionada ?? audio.emisoraActual;
|
Emisora? get emisoraActual => _emisoraSeleccionada ?? audio.emisoraActual;
|
||||||
|
Emisora? get emisoraPreferida => _resolverEmisoraPreferida();
|
||||||
|
String? get emisoraPreferidaUuid => emisoraPreferida?.uuid;
|
||||||
Stream<EstadoReproduccion> get estadoStream => audio.estadoStream;
|
Stream<EstadoReproduccion> get estadoStream => audio.estadoStream;
|
||||||
PresetEcualizador get presetEcualizador => _presetActual;
|
PresetEcualizador get presetEcualizador => _presetActual;
|
||||||
PresetEcualizador get presetPrincipalEcualizador => _presetPrincipal;
|
PresetEcualizador get presetPrincipalEcualizador => _presetPrincipal;
|
||||||
@@ -134,6 +139,29 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
return mapa.values.toList();
|
return mapa.values.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Emisora> get emisorasDisponiblesPreferencia {
|
||||||
|
final mapa = <String, Emisora>{};
|
||||||
|
for (final emisora in _listaFavoritos) {
|
||||||
|
mapa[emisora.uuid] = emisora;
|
||||||
|
}
|
||||||
|
for (final emisora in _emisorasCustom) {
|
||||||
|
mapa.putIfAbsent(emisora.uuid, () => emisora);
|
||||||
|
}
|
||||||
|
for (final emisora in _populares) {
|
||||||
|
mapa.putIfAbsent(emisora.uuid, () => emisora);
|
||||||
|
}
|
||||||
|
for (final emisora in _tendencias) {
|
||||||
|
mapa.putIfAbsent(emisora.uuid, () => emisora);
|
||||||
|
}
|
||||||
|
for (final emisora in _resultadosBusqueda) {
|
||||||
|
mapa.putIfAbsent(emisora.uuid, () => emisora);
|
||||||
|
}
|
||||||
|
for (final emisora in _emisorasCercanas) {
|
||||||
|
mapa.putIfAbsent(emisora.uuid, () => emisora);
|
||||||
|
}
|
||||||
|
return mapa.values.toList();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> inicializar() {
|
Future<void> inicializar() {
|
||||||
_initFuture ??= _init();
|
_initFuture ??= _init();
|
||||||
return _initFuture!;
|
return _initFuture!;
|
||||||
@@ -142,11 +170,13 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
await grabacion.inicializar();
|
await grabacion.inicializar();
|
||||||
await _cargarEcualizadorPersistido();
|
await _cargarEcualizadorPersistido();
|
||||||
|
await _cargarEmisoraPreferida();
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
cargarPopulares(),
|
cargarPopulares(),
|
||||||
cargarFavoritos(),
|
cargarFavoritos(),
|
||||||
_cargarEmisorasCustom(),
|
_cargarEmisorasCustom(),
|
||||||
]);
|
]);
|
||||||
|
await _normalizarEmisoraPreferida();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Escucha el stream de estado del audio y gestiona errores de reproducción.
|
/// Escucha el stream de estado del audio y gestiona errores de reproducción.
|
||||||
@@ -209,9 +239,61 @@ class EstadoRadio extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> cargarFavoritos() async {
|
Future<void> cargarFavoritos() async {
|
||||||
_listaFavoritos = await favoritos.obtenerTodos();
|
_listaFavoritos = await favoritos.obtenerTodos();
|
||||||
|
await _normalizarEmisoraPreferida();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> cambiarEmisoraPreferida(Emisora? emisora) async {
|
||||||
|
_emisoraPreferidaUuid = emisora?.uuid;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (_emisoraPreferidaUuid == null) {
|
||||||
|
await prefs.remove(_keyEmisoraPreferida);
|
||||||
|
} else {
|
||||||
|
await prefs.setString(_keyEmisoraPreferida, _emisoraPreferidaUuid!);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reproducirEmisoraPreferida() async {
|
||||||
|
final preferida = emisoraPreferida;
|
||||||
|
if (preferida == null) return;
|
||||||
|
await reproducir(preferida);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _cargarEmisoraPreferida() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
_emisoraPreferidaUuid = prefs.getString(_keyEmisoraPreferida);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _normalizarEmisoraPreferida() async {
|
||||||
|
final preferida = _resolverEmisoraPreferida();
|
||||||
|
if (preferida?.uuid == _emisoraPreferidaUuid) return;
|
||||||
|
_emisoraPreferidaUuid = preferida?.uuid;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (_emisoraPreferidaUuid == null) {
|
||||||
|
await prefs.remove(_keyEmisoraPreferida);
|
||||||
|
} else {
|
||||||
|
await prefs.setString(_keyEmisoraPreferida, _emisoraPreferidaUuid!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Emisora? _resolverEmisoraPreferida() {
|
||||||
|
final uuid = _emisoraPreferidaUuid;
|
||||||
|
if (uuid != null) {
|
||||||
|
for (final emisora in _listaFavoritos) {
|
||||||
|
if (emisora.uuid == uuid) return emisora;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_listaFavoritos.isNotEmpty) return _listaFavoritos.first;
|
||||||
|
if (uuid != null) {
|
||||||
|
for (final emisora in emisorasDisponiblesPreferencia) {
|
||||||
|
if (emisora.uuid == uuid) return emisora;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final disponibles = emisorasDisponiblesPreferencia;
|
||||||
|
return disponibles.isEmpty ? null : disponibles.first;
|
||||||
|
}
|
||||||
|
|
||||||
static const int _tamanoPaginaBusqueda = 30;
|
static const int _tamanoPaginaBusqueda = 30;
|
||||||
static const int _maxResultadosBusquedaEnMemoria = 180;
|
static const int _maxResultadosBusquedaEnMemoria = 180;
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ class _AjustesContent extends StatelessWidget {
|
|||||||
SizedBox(height: 12),
|
SizedBox(height: 12),
|
||||||
_SeccionGrabaciones(),
|
_SeccionGrabaciones(),
|
||||||
SizedBox(height: 12),
|
SizedBox(height: 12),
|
||||||
|
_SeccionEmisoraPreferida(),
|
||||||
|
SizedBox(height: 12),
|
||||||
_SeccionEmisoras(),
|
_SeccionEmisoras(),
|
||||||
SizedBox(height: 12),
|
SizedBox(height: 12),
|
||||||
_SeccionBackup(),
|
_SeccionBackup(),
|
||||||
@@ -238,6 +240,107 @@ class _SeccionEcualizador extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SeccionEmisoraPreferida extends StatelessWidget {
|
||||||
|
const _SeccionEmisoraPreferida();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final estado = context.watch<EstadoRadio>();
|
||||||
|
final favoritas = estado.listaFavoritos;
|
||||||
|
final preferida = estado.emisoraPreferida;
|
||||||
|
final opciones = _opciones(estado, preferida);
|
||||||
|
|
||||||
|
return PluriGlassSurface(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.radio_rounded),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'Emisora preferida',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Se preselecciona al crear alarmas y puede iniciarse como reproducción rápida.',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
if (opciones.isEmpty)
|
||||||
|
const ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: Icon(Icons.info_outline_rounded),
|
||||||
|
title: Text('Todavía no hay emisoras disponibles'),
|
||||||
|
subtitle: Text(
|
||||||
|
'Guardá favoritas o cargá emisoras para elegir una preferida.',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
initialValue: preferida?.uuid,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText:
|
||||||
|
favoritas.isEmpty
|
||||||
|
? 'Fallback automático'
|
||||||
|
: 'Favorita por defecto',
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
for (final emisora in opciones)
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: emisora.uuid,
|
||||||
|
child: Text(
|
||||||
|
emisora.nombre,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (uuid) async {
|
||||||
|
final seleccion = opciones.firstWhere((e) => e.uuid == uuid);
|
||||||
|
await context.read<EstadoRadio>().cambiarEmisoraPreferida(
|
||||||
|
seleccion,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (preferida != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
favoritas.any((e) => e.uuid == preferida.uuid)
|
||||||
|
? 'Preferida actual: ${preferida.nombre}'
|
||||||
|
: 'Sin favoritas: usando automáticamente ${preferida.nombre}',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: FilledButton.tonalIcon(
|
||||||
|
icon: const Icon(Icons.play_arrow_rounded),
|
||||||
|
label: const Text('Reproducir preferida'),
|
||||||
|
onPressed:
|
||||||
|
() => context.read<EstadoRadio>().reproducirEmisoraPreferida(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Emisora> _opciones(EstadoRadio estado, Emisora? preferida) {
|
||||||
|
final base =
|
||||||
|
estado.listaFavoritos.isNotEmpty
|
||||||
|
? estado.listaFavoritos
|
||||||
|
: estado.emisorasDisponiblesPreferencia;
|
||||||
|
final mapa = <String, Emisora>{for (final emisora in base) emisora.uuid: emisora};
|
||||||
|
if (preferida != null) {
|
||||||
|
mapa[preferida.uuid] = preferida;
|
||||||
|
}
|
||||||
|
return mapa.values.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SeccionEmisoras extends StatelessWidget {
|
class _SeccionEmisoras extends StatelessWidget {
|
||||||
const _SeccionEmisoras();
|
const _SeccionEmisoras();
|
||||||
|
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
|||||||
late bool _sonarEnVacaciones;
|
late bool _sonarEnVacaciones;
|
||||||
late SonidoInternoAlarma _sonidoInterno;
|
late SonidoInternoAlarma _sonidoInterno;
|
||||||
Emisora? _emisora;
|
Emisora? _emisora;
|
||||||
|
bool _favoritosSolicitados = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -311,7 +312,7 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
|||||||
_volumen = alarma?.volumen ?? 0.85;
|
_volumen = alarma?.volumen ?? 0.85;
|
||||||
_sonarEnVacaciones = alarma?.sonarEnVacaciones ?? true;
|
_sonarEnVacaciones = alarma?.sonarEnVacaciones ?? true;
|
||||||
_sonidoInterno = alarma?.sonidoInterno ?? SonidoInternoAlarma.amanecer;
|
_sonidoInterno = alarma?.sonidoInterno ?? SonidoInternoAlarma.amanecer;
|
||||||
_emisora = alarma?.emisora;
|
_emisora = alarma?.emisora ?? context.read<EstadoRadio>().emisoraPreferida;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -324,6 +325,20 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final radio = context.watch<EstadoRadio>();
|
final radio = context.watch<EstadoRadio>();
|
||||||
final bottom = MediaQuery.of(context).viewInsets.bottom;
|
final bottom = MediaQuery.of(context).viewInsets.bottom;
|
||||||
|
if (!_favoritosSolicitados) {
|
||||||
|
_favoritosSolicitados = true;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) context.read<EstadoRadio>().cargarFavoritos();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (_emisora == null && widget.alarma == null && radio.emisoraPreferida != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted && _emisora == null) {
|
||||||
|
setState(() => _emisora = radio.emisoraPreferida);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
final favoritas = _favoritasConSeleccion(radio.listaFavoritos);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.fromLTRB(12, 12, 12, bottom + 12),
|
padding: EdgeInsets.fromLTRB(12, 12, 12, bottom + 12),
|
||||||
child: PluriGlassSurface(
|
child: PluriGlassSurface(
|
||||||
@@ -437,22 +452,52 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
|||||||
onChanged: (value) => setState(() => _sonidoInterno = value ?? _sonidoInterno),
|
onChanged: (value) => setState(() => _sonidoInterno = value ?? _sonidoInterno),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ListTile(
|
DropdownButtonFormField<String>(
|
||||||
contentPadding: EdgeInsets.zero,
|
key: ValueKey(_emisora?.uuid ?? 'sin-emisora'),
|
||||||
leading: const Icon(Icons.radio_rounded),
|
initialValue: _emisora?.uuid,
|
||||||
title: Text(_emisora?.nombre ?? 'Sin emisora principal'),
|
decoration: const InputDecoration(
|
||||||
subtitle: Text(
|
labelText: 'Emisora favorita',
|
||||||
radio.emisoraActual == null
|
prefixIcon: Icon(Icons.radio_rounded),
|
||||||
? 'Se usará el sonido interno si la radio falla.'
|
|
||||||
: 'Podés usar la emisora que está seleccionada ahora.',
|
|
||||||
),
|
|
||||||
trailing: FilledButton.tonal(
|
|
||||||
onPressed: radio.emisoraActual == null
|
|
||||||
? null
|
|
||||||
: () => setState(() => _emisora = radio.emisoraActual),
|
|
||||||
child: const Text('Usar actual'),
|
|
||||||
),
|
),
|
||||||
|
items: [
|
||||||
|
const DropdownMenuItem<String>(
|
||||||
|
value: '',
|
||||||
|
child: Text('Sin emisora: usar sonido interno'),
|
||||||
|
),
|
||||||
|
for (final emisora in favoritas)
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: emisora.uuid,
|
||||||
|
child: Text(
|
||||||
|
emisora.nombre,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (uuid) => setState(() {
|
||||||
|
if (uuid == null || uuid.isEmpty) {
|
||||||
|
_emisora = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_emisora = favoritas.firstWhere((e) => e.uuid == uuid);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
|
if (favoritas.isEmpty) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
const Text(
|
||||||
|
'Guardá emisoras en Favoritos para usarlas como alarma musical.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (radio.emisoraActual != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: FilledButton.tonalIcon(
|
||||||
|
onPressed: () => setState(() => _emisora = radio.emisoraActual),
|
||||||
|
icon: const Icon(Icons.add_task_rounded),
|
||||||
|
label: const Text('Usar emisora actual'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
SwitchListTile.adaptive(
|
SwitchListTile.adaptive(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
@@ -531,6 +576,18 @@ class _EditorAlarmaSheetState extends State<_EditorAlarmaSheet> {
|
|||||||
await estado.guardarAlarma(alarma);
|
await estado.guardarAlarma(alarma);
|
||||||
if (mounted) Navigator.pop(context);
|
if (mounted) Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Emisora> _favoritasConSeleccion(List<Emisora> favoritas) {
|
||||||
|
final mapa = <String, Emisora>{};
|
||||||
|
for (final emisora in favoritas) {
|
||||||
|
mapa[emisora.uuid] = emisora;
|
||||||
|
}
|
||||||
|
final seleccionada = _emisora;
|
||||||
|
if (seleccionada != null) {
|
||||||
|
mapa[seleccionada.uuid] = seleccionada;
|
||||||
|
}
|
||||||
|
return mapa.values.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AccesoDiagnostico extends StatelessWidget {
|
class _AccesoDiagnostico extends StatelessWidget {
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ class ServicioAlarmasAndroid {
|
|||||||
Future<void> cancelar(String alarmaId) =>
|
Future<void> cancelar(String alarmaId) =>
|
||||||
_channel.invokeMethod<void>('cancelAlarm', {'id': alarmaId});
|
_channel.invokeMethod<void>('cancelAlarm', {'id': alarmaId});
|
||||||
|
|
||||||
|
Future<void> ocultarNotificacionAlarma(String alarmaId) => _channel
|
||||||
|
.invokeMethod<void>('dismissAlarmNotification', {'id': alarmaId});
|
||||||
|
|
||||||
Future<DiagnosticoAlarmasAndroid> diagnostico() async {
|
Future<DiagnosticoAlarmasAndroid> diagnostico() async {
|
||||||
final raw = await _channel.invokeMethod<Map<Object?, Object?>>(
|
final raw = await _channel.invokeMethod<Map<Object?, Object?>>(
|
||||||
'diagnostics',
|
'diagnostics',
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class PluriWaveScaffold extends StatelessWidget {
|
|||||||
appBar: appBar,
|
appBar: appBar,
|
||||||
bottomNavigationBar: bottomNavigationBar,
|
bottomNavigationBar: bottomNavigationBar,
|
||||||
floatingActionButton: floatingActionButton,
|
floatingActionButton: floatingActionButton,
|
||||||
extendBody: true,
|
extendBody: false,
|
||||||
body: DecoratedBox(
|
body: DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
|
|||||||
Reference in New Issue
Block a user