Compare commits
2
Commits
71978de68f
...
4ffd73d136
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ffd73d136 | ||
|
|
58922de6fc |
@@ -39,7 +39,8 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
snoozeMinutes: Int = 5,
|
snoozeMinutes: Int = 5,
|
||||||
fallbackStationName: String? = null,
|
fallbackStationName: String? = null,
|
||||||
fallbackStationUrl: String? = null,
|
fallbackStationUrl: String? = null,
|
||||||
fadeInSegundos: Int = 0
|
fadeInSegundos: Int = 0,
|
||||||
|
preNoticeTemplate: String? = null
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val existing = readSpec(id)
|
val existing = readSpec(id)
|
||||||
val preservedSnooze = preserveNativeSnooze(
|
val preservedSnooze = preserveNativeSnooze(
|
||||||
@@ -70,7 +71,8 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
fallbackSound = fallbackSound,
|
fallbackSound = fallbackSound,
|
||||||
volume = volume.coerceIn(0f, 1f),
|
volume = volume.coerceIn(0f, 1f),
|
||||||
fadeInSegundos = fadeInSegundos.coerceIn(0, 60),
|
fadeInSegundos = fadeInSegundos.coerceIn(0, 60),
|
||||||
timezoneId = TimeZone.getDefault().id
|
timezoneId = TimeZone.getDefault().id,
|
||||||
|
preNoticeTemplate = preNoticeTemplate
|
||||||
)
|
)
|
||||||
return scheduleSpec(spec, persistOnSuccess = true)
|
return scheduleSpec(spec, persistOnSuccess = true)
|
||||||
}
|
}
|
||||||
@@ -145,6 +147,7 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT,
|
PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT,
|
||||||
spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
||||||
)
|
)
|
||||||
|
putExtra(EXTRA_PRE_NOTICE_TEMPLATE, spec.preNoticeTemplate)
|
||||||
},
|
},
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
@@ -165,6 +168,7 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT,
|
PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT,
|
||||||
spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
||||||
)
|
)
|
||||||
|
putExtra(EXTRA_PRE_NOTICE_TEMPLATE, spec.preNoticeTemplate)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Log.d(tag, "alarm.schedule preNotice immediate id=${spec.id}")
|
Log.d(tag, "alarm.schedule preNotice immediate id=${spec.id}")
|
||||||
@@ -652,7 +656,10 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
val fallbackSound: String?,
|
val fallbackSound: String?,
|
||||||
val volume: Float,
|
val volume: Float,
|
||||||
val fadeInSegundos: Int = 0,
|
val fadeInSegundos: Int = 0,
|
||||||
val timezoneId: String
|
val timezoneId: String,
|
||||||
|
// Nullable for backward compat: old persisted alarms without this field
|
||||||
|
// fall back to the English default in the receiver. Schema stays v3.
|
||||||
|
val preNoticeTemplate: String? = null
|
||||||
) {
|
) {
|
||||||
fun toJson(): JSONObject = JSONObject().apply {
|
fun toJson(): JSONObject = JSONObject().apply {
|
||||||
put("schemaVersion", 3)
|
put("schemaVersion", 3)
|
||||||
@@ -679,6 +686,7 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
put("volume", volume)
|
put("volume", volume)
|
||||||
put("fadeInSegundos", fadeInSegundos)
|
put("fadeInSegundos", fadeInSegundos)
|
||||||
put("timezoneId", timezoneId)
|
put("timezoneId", timezoneId)
|
||||||
|
put("preNoticeTemplate", preNoticeTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -715,7 +723,8 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
fallbackSound = json.optString("fallbackSound").takeIf { it.isNotBlank() },
|
fallbackSound = json.optString("fallbackSound").takeIf { it.isNotBlank() },
|
||||||
volume = json.optDouble("volume", 0.85).toFloat(),
|
volume = json.optDouble("volume", 0.85).toFloat(),
|
||||||
fadeInSegundos = json.optInt("fadeInSegundos", 0).coerceIn(0, 60),
|
fadeInSegundos = json.optInt("fadeInSegundos", 0).coerceIn(0, 60),
|
||||||
timezoneId = json.optString("timezoneId", TimeZone.getDefault().id)
|
timezoneId = json.optString("timezoneId", TimeZone.getDefault().id),
|
||||||
|
preNoticeTemplate = json.optString("preNoticeTemplate").takeIf { it.isNotBlank() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -730,6 +739,9 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
private const val PRE_NOTICE_MILLIS = 30 * 60 * 1000L
|
private const val PRE_NOTICE_MILLIS = 30 * 60 * 1000L
|
||||||
private const val SCHEDULE_UNICA = "unica"
|
private const val SCHEDULE_UNICA = "unica"
|
||||||
private const val SCHEDULE_DIAS_SEMANA = "diasSemana"
|
private const val SCHEDULE_DIAS_SEMANA = "diasSemana"
|
||||||
|
// Intent extra key for the localized pre-notice template string.
|
||||||
|
// Declared once here; PluriWaveAlarmReceiver reads it via this constant.
|
||||||
|
const val EXTRA_PRE_NOTICE_TEMPLATE = "preNoticeTemplate"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ class MainActivity : AudioServiceActivity() {
|
|||||||
snoozeMinutes = call.argument<Int>("snoozeMinutes") ?: 5,
|
snoozeMinutes = call.argument<Int>("snoozeMinutes") ?: 5,
|
||||||
fallbackStationName = call.argument<String>("fallbackStationName"),
|
fallbackStationName = call.argument<String>("fallbackStationName"),
|
||||||
fallbackStationUrl = call.argument<String>("fallbackStationUrl"),
|
fallbackStationUrl = call.argument<String>("fallbackStationUrl"),
|
||||||
fadeInSegundos = call.argument<Int>("fadeInSegundos") ?: 0
|
fadeInSegundos = call.argument<Int>("fadeInSegundos") ?: 0,
|
||||||
|
preNoticeTemplate = call.argument<String>("preNoticeTemplate")
|
||||||
)
|
)
|
||||||
result.success(scheduled)
|
result.success(scheduled)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
title,
|
title,
|
||||||
snoozeMinutes,
|
snoozeMinutes,
|
||||||
intent.getLongExtra(EXTRA_TRIGGER_AT, 0L),
|
intent.getLongExtra(EXTRA_TRIGGER_AT, 0L),
|
||||||
intent.getLongExtra(EXTRA_OCCURRENCE_AT, 0L)
|
intent.getLongExtra(EXTRA_OCCURRENCE_AT, 0L),
|
||||||
|
intent.getStringExtra(AlarmScheduler.EXTRA_PRE_NOTICE_TEMPLATE)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ACTION_POSTPONE_NEXT -> {
|
ACTION_POSTPONE_NEXT -> {
|
||||||
@@ -100,10 +101,14 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
title: String,
|
title: String,
|
||||||
snoozeMinutes: Int,
|
snoozeMinutes: Int,
|
||||||
triggerAtMillis: Long,
|
triggerAtMillis: Long,
|
||||||
occurrenceAtMillis: Long
|
occurrenceAtMillis: Long,
|
||||||
|
preNoticeTemplate: String? = null
|
||||||
) {
|
) {
|
||||||
ensureChannel(context)
|
ensureChannel(context)
|
||||||
|
|
||||||
|
val remaining = computeRemainingMinutes(triggerAtMillis)
|
||||||
|
val contentText = formatPreNoticeText(preNoticeTemplate, remaining)
|
||||||
|
|
||||||
val openAppIntent = PendingIntent.getActivity(
|
val openAppIntent = PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
requestCode(alarmId, 1),
|
requestCode(alarmId, 1),
|
||||||
@@ -144,7 +149,7 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setContentText("Empieza en 30 minutos")
|
.setContentText(contentText)
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.setCategory(NotificationCompat.CATEGORY_REMINDER)
|
.setCategory(NotificationCompat.CATEGORY_REMINDER)
|
||||||
.setSilent(true)
|
.setSilent(true)
|
||||||
@@ -156,12 +161,31 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
NotificationManagerCompat.from(context).notify(notificationIdForAlarm(alarmId), notification)
|
NotificationManagerCompat.from(context).notify(notificationIdForAlarm(alarmId), notification)
|
||||||
Log.d(TAG, "alarm.notification preNotice shown id=$alarmId")
|
Log.d(TAG, "alarm.notification preNotice shown id=$alarmId remaining=$remaining")
|
||||||
} catch (error: SecurityException) {
|
} catch (error: SecurityException) {
|
||||||
Log.e(TAG, "alarm.notification preNotice SecurityException id=$alarmId", error)
|
Log.e(TAG, "alarm.notification preNotice SecurityException id=$alarmId", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the number of minutes remaining until [triggerAtMillis],
|
||||||
|
* clamped to a minimum of 1. Handles Doze-delayed wakeups and clock drift.
|
||||||
|
*/
|
||||||
|
private fun computeRemainingMinutes(triggerAtMillis: Long): Long =
|
||||||
|
maxOf(1L, (triggerAtMillis - System.currentTimeMillis()) / 60_000L)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the pre-notice notification text by replacing the `{minutes}`
|
||||||
|
* placeholder in [template] with [remaining]. Falls back to an English
|
||||||
|
* default if [template] is null or blank.
|
||||||
|
*/
|
||||||
|
private fun formatPreNoticeText(template: String?, remaining: Long): String {
|
||||||
|
if (template.isNullOrBlank()) {
|
||||||
|
return "Starts in $remaining min"
|
||||||
|
}
|
||||||
|
return template.replace("{minutes}", remaining.toString())
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ class EstadoEcualizador extends ChangeNotifier {
|
|||||||
/// Matrix presets: "stationUuid:deviceId" → PresetEcualizador.
|
/// Matrix presets: "stationUuid:deviceId" → PresetEcualizador.
|
||||||
final Map<String, PresetEcualizador> _presetsMatriz = {};
|
final Map<String, PresetEcualizador> _presetsMatriz = {};
|
||||||
|
|
||||||
|
/// Custom display names for devices: deviceId → custom name.
|
||||||
|
final Map<String, String> _nombresDispositivos = {};
|
||||||
|
|
||||||
PresetEcualizador _presetPrincipal = PresetEcualizador.flat;
|
PresetEcualizador _presetPrincipal = PresetEcualizador.flat;
|
||||||
PresetEcualizador _presetActual = PresetEcualizador.flat;
|
PresetEcualizador _presetActual = PresetEcualizador.flat;
|
||||||
bool _activo = true;
|
bool _activo = true;
|
||||||
@@ -65,6 +68,9 @@ class EstadoEcualizador extends ChangeNotifier {
|
|||||||
bool get eqMultiDeviceEnabled => _eqMultiDeviceEnabled;
|
bool get eqMultiDeviceEnabled => _eqMultiDeviceEnabled;
|
||||||
String? get dispositivoActualId => _dispositivoActualId;
|
String? get dispositivoActualId => _dispositivoActualId;
|
||||||
|
|
||||||
|
Map<String, String> get nombresDispositivos =>
|
||||||
|
Map.unmodifiable(_nombresDispositivos);
|
||||||
|
|
||||||
Map<String, PresetEcualizador> get presetsPorEmisora =>
|
Map<String, PresetEcualizador> get presetsPorEmisora =>
|
||||||
Map.unmodifiable(_presetsEmisoraMap);
|
Map.unmodifiable(_presetsEmisoraMap);
|
||||||
|
|
||||||
@@ -148,6 +154,9 @@ class EstadoEcualizador extends ChangeNotifier {
|
|||||||
_presetsMatriz
|
_presetsMatriz
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(config.presetsMatriz);
|
..addAll(config.presetsMatriz);
|
||||||
|
_nombresDispositivos
|
||||||
|
..clear()
|
||||||
|
..addAll(config.nombresDispositivos);
|
||||||
|
|
||||||
// Resolve active preset and apply it.
|
// Resolve active preset and apply it.
|
||||||
_presetActual = _resolverPresetActivo();
|
_presetActual = _resolverPresetActivo();
|
||||||
@@ -156,6 +165,11 @@ class EstadoEcualizador extends ChangeNotifier {
|
|||||||
|
|
||||||
// Subscribe to device changes only when toggle is on.
|
// Subscribe to device changes only when toggle is on.
|
||||||
_configurarSuscripcionDispositivo();
|
_configurarSuscripcionDispositivo();
|
||||||
|
|
||||||
|
// Seed current device immediately so presets resolve before first event.
|
||||||
|
if (_eqMultiDeviceEnabled) {
|
||||||
|
await _sembrarDispositivoActual();
|
||||||
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
_presetPrincipal = PresetEcualizador.flat;
|
_presetPrincipal = PresetEcualizador.flat;
|
||||||
_presetActual = PresetEcualizador.flat;
|
_presetActual = PresetEcualizador.flat;
|
||||||
@@ -164,6 +178,20 @@ class EstadoEcualizador extends ChangeNotifier {
|
|||||||
_presetsEmisoraMap.clear();
|
_presetsEmisoraMap.clear();
|
||||||
_presetsDispositivo.clear();
|
_presetsDispositivo.clear();
|
||||||
_presetsMatriz.clear();
|
_presetsMatriz.clear();
|
||||||
|
_nombresDispositivos.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queries the current device and seeds [_dispositivoActualId] without
|
||||||
|
/// waiting for a stream event. Falls back to `'builtin_speaker'` on error.
|
||||||
|
Future<void> _sembrarDispositivoActual() async {
|
||||||
|
final svc = _dispositivoAudio;
|
||||||
|
if (svc == null) return;
|
||||||
|
try {
|
||||||
|
final dispositivo = await svc.obtenerDispositivoActual();
|
||||||
|
await _onDispositivoCambiado(dispositivo);
|
||||||
|
} catch (_) {
|
||||||
|
_dispositivoActualId = 'builtin_speaker';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,6 +308,55 @@ class EstadoEcualizador extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Persists a custom display name for [deviceId].
|
||||||
|
///
|
||||||
|
/// Empty names are silently ignored so the existing name is preserved.
|
||||||
|
/// No-op when the multi-device toggle is off.
|
||||||
|
Future<void> renombrarDispositivo(String deviceId, String nombre) async {
|
||||||
|
if (!_eqMultiDeviceEnabled) return;
|
||||||
|
final nombreTrimmed = nombre.trim();
|
||||||
|
if (nombreTrimmed.isEmpty) return;
|
||||||
|
_nombresDispositivos[deviceId] = nombreTrimmed;
|
||||||
|
await servicio.guardarNombresDispositivos(
|
||||||
|
Map.unmodifiable(_nombresDispositivos),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persists a per-device EQ preset for [deviceId].
|
||||||
|
///
|
||||||
|
/// Guards on [_eqMultiDeviceEnabled]: no-op when toggle is off.
|
||||||
|
/// If [deviceId] is the currently active device, re-resolves and
|
||||||
|
/// applies the effective preset immediately.
|
||||||
|
Future<void> guardarPresetDispositivo(
|
||||||
|
String deviceId,
|
||||||
|
PresetEcualizador preset,
|
||||||
|
) async {
|
||||||
|
if (!_eqMultiDeviceEnabled) return;
|
||||||
|
_presetsDispositivo[deviceId] = preset;
|
||||||
|
await servicio.guardarPresetDispositivo(deviceId, preset);
|
||||||
|
if (_dispositivoActualId == deviceId) {
|
||||||
|
// Re-resolve using the full hierarchy (station/matrix may override).
|
||||||
|
final resuelto = _resolverPresetActivo();
|
||||||
|
_presetActual = resuelto;
|
||||||
|
await audio.aplicarPreset(resuelto);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the stored custom name for [deviceId], or an empty string if none.
|
||||||
|
String obtenerNombreDispositivo(String deviceId) =>
|
||||||
|
_nombresDispositivos[deviceId] ?? '';
|
||||||
|
|
||||||
|
/// Resolves the display name for [deviceId] using the fallback chain:
|
||||||
|
/// custom name → [platformName] → raw [deviceId].
|
||||||
|
String nombreVisible(String deviceId, String platformName) {
|
||||||
|
final custom = _nombresDispositivos[deviceId];
|
||||||
|
if (custom != null && custom.isNotEmpty) return custom;
|
||||||
|
if (platformName.isNotEmpty) return platformName;
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> cambiarActivo(bool activo) async {
|
Future<void> cambiarActivo(bool activo) async {
|
||||||
_activo = activo;
|
_activo = activo;
|
||||||
await servicio.guardarActivo(activo);
|
await servicio.guardarActivo(activo);
|
||||||
|
|||||||
+15
-2
@@ -627,5 +627,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"preNoticeCountdown": "يبدأ خلال {minutes} دقيقة",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "تعديل الجهاز",
|
||||||
|
"eqDeviceNameLabel": "اسم الجهاز",
|
||||||
|
"eqDeviceNameHint": "مثال: مكبر صوت غرفة المعيشة",
|
||||||
|
"eqDeviceNameConfirm": "حفظ",
|
||||||
|
"eqDeviceConnected": "متصل"
|
||||||
|
}
|
||||||
+15
-2
@@ -627,5 +627,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"preNoticeCountdown": "{minutes} মিনিটে শুরু হবে",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "ডিভাইস সম্পাদনা করুন",
|
||||||
|
"eqDeviceNameLabel": "ডিভাইসের নাম",
|
||||||
|
"eqDeviceNameHint": "যেমন: লিভিং রুমের স্পিকার",
|
||||||
|
"eqDeviceNameConfirm": "সংরক্ষণ করুন",
|
||||||
|
"eqDeviceConnected": "সংযুক্ত"
|
||||||
|
}
|
||||||
+15
-2
@@ -627,5 +627,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"preNoticeCountdown": "Startet in {minutes} Min.",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "Gerät bearbeiten",
|
||||||
|
"eqDeviceNameLabel": "Gerätename",
|
||||||
|
"eqDeviceNameHint": "z. B. Wohnzimmer-Lautsprecher",
|
||||||
|
"eqDeviceNameConfirm": "Speichern",
|
||||||
|
"eqDeviceConnected": "Verbunden"
|
||||||
|
}
|
||||||
+14
-1
@@ -627,5 +627,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"preNoticeCountdown": "Starts in {minutes} min",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "Edit device",
|
||||||
|
"eqDeviceNameLabel": "Device name",
|
||||||
|
"eqDeviceNameHint": "e.g. Living Room Speaker",
|
||||||
|
"eqDeviceNameConfirm": "Save",
|
||||||
|
"eqDeviceConnected": "Connected"
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -590,5 +590,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"preNoticeCountdown": "Empieza en {minutes} min",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "Editar dispositivo",
|
||||||
|
"eqDeviceNameLabel": "Nombre del dispositivo",
|
||||||
|
"eqDeviceNameHint": "Ej: Altavoz del living",
|
||||||
|
"eqDeviceNameConfirm": "Guardar",
|
||||||
|
"eqDeviceConnected": "Conectado"
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-2
@@ -627,5 +627,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"preNoticeCountdown": "Démarre dans {minutes} min",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "Modifier l'appareil",
|
||||||
|
"eqDeviceNameLabel": "Nom de l'appareil",
|
||||||
|
"eqDeviceNameHint": "ex. : Enceinte salon",
|
||||||
|
"eqDeviceNameConfirm": "Enregistrer",
|
||||||
|
"eqDeviceConnected": "Connecté"
|
||||||
|
}
|
||||||
+15
-2
@@ -627,5 +627,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"preNoticeCountdown": "{minutes} मिनट में शुरू होगा",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "डिवाइस संपादित करें",
|
||||||
|
"eqDeviceNameLabel": "डिवाइस का नाम",
|
||||||
|
"eqDeviceNameHint": "उदा. लिविंग रूम स्पीकर",
|
||||||
|
"eqDeviceNameConfirm": "सहेजें",
|
||||||
|
"eqDeviceConnected": "कनेक्टेड"
|
||||||
|
}
|
||||||
+15
-2
@@ -627,5 +627,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"preNoticeCountdown": "Mulai dalam {minutes} menit",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "Edit perangkat",
|
||||||
|
"eqDeviceNameLabel": "Nama perangkat",
|
||||||
|
"eqDeviceNameHint": "Mis: Speaker ruang tamu",
|
||||||
|
"eqDeviceNameConfirm": "Simpan",
|
||||||
|
"eqDeviceConnected": "Terhubung"
|
||||||
|
}
|
||||||
+15
-2
@@ -627,5 +627,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"preNoticeCountdown": "Inizia tra {minutes} min",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "Modifica dispositivo",
|
||||||
|
"eqDeviceNameLabel": "Nome del dispositivo",
|
||||||
|
"eqDeviceNameHint": "Es: Cassa del salotto",
|
||||||
|
"eqDeviceNameConfirm": "Salva",
|
||||||
|
"eqDeviceConnected": "Connesso"
|
||||||
|
}
|
||||||
+15
-2
@@ -627,5 +627,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"preNoticeCountdown": "{minutes}分後に開始",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "デバイスを編集",
|
||||||
|
"eqDeviceNameLabel": "デバイス名",
|
||||||
|
"eqDeviceNameHint": "例:リビングのスピーカー",
|
||||||
|
"eqDeviceNameConfirm": "保存",
|
||||||
|
"eqDeviceConnected": "接続中"
|
||||||
|
}
|
||||||
+15
-2
@@ -627,5 +627,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"preNoticeCountdown": "Começa em {minutes} min",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "Editar dispositivo",
|
||||||
|
"eqDeviceNameLabel": "Nome do dispositivo",
|
||||||
|
"eqDeviceNameHint": "Ex: Caixa da sala",
|
||||||
|
"eqDeviceNameConfirm": "Salvar",
|
||||||
|
"eqDeviceConnected": "Conectado"
|
||||||
|
}
|
||||||
+15
-2
@@ -627,5 +627,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"preNoticeCountdown": "Начнётся через {minutes} мин",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "Изменить устройство",
|
||||||
|
"eqDeviceNameLabel": "Название устройства",
|
||||||
|
"eqDeviceNameHint": "Напр.: Колонка в гостиной",
|
||||||
|
"eqDeviceNameConfirm": "Сохранить",
|
||||||
|
"eqDeviceConnected": "Подключено"
|
||||||
|
}
|
||||||
+15
-2
@@ -627,5 +627,18 @@
|
|||||||
"placeholders": {
|
"placeholders": {
|
||||||
"presetName": {}
|
"presetName": {}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"preNoticeCountdown": "{minutes}分钟后开始",
|
||||||
|
"@preNoticeCountdown": {
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eqDeviceEditTitle": "编辑设备",
|
||||||
|
"eqDeviceNameLabel": "设备名称",
|
||||||
|
"eqDeviceNameHint": "例:客厅音箱",
|
||||||
|
"eqDeviceNameConfirm": "保存",
|
||||||
|
"eqDeviceConnected": "已连接"
|
||||||
|
}
|
||||||
@@ -2305,6 +2305,42 @@ abstract class AppLocalizations {
|
|||||||
/// In es, this message translates to:
|
/// In es, this message translates to:
|
||||||
/// **'Preset: {presetName}'**
|
/// **'Preset: {presetName}'**
|
||||||
String advancedEqDevicePresetLabel(Object presetName);
|
String advancedEqDevicePresetLabel(Object presetName);
|
||||||
|
|
||||||
|
/// No description provided for @preNoticeCountdown.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Empieza en {minutes} min'**
|
||||||
|
String preNoticeCountdown(int minutes);
|
||||||
|
|
||||||
|
/// No description provided for @eqDeviceEditTitle.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Editar dispositivo'**
|
||||||
|
String get eqDeviceEditTitle;
|
||||||
|
|
||||||
|
/// No description provided for @eqDeviceNameLabel.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Nombre del dispositivo'**
|
||||||
|
String get eqDeviceNameLabel;
|
||||||
|
|
||||||
|
/// No description provided for @eqDeviceNameHint.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Ej: Altavoz del living'**
|
||||||
|
String get eqDeviceNameHint;
|
||||||
|
|
||||||
|
/// No description provided for @eqDeviceNameConfirm.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Guardar'**
|
||||||
|
String get eqDeviceNameConfirm;
|
||||||
|
|
||||||
|
/// No description provided for @eqDeviceConnected.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Conectado'**
|
||||||
|
String get eqDeviceConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -1247,4 +1247,24 @@ class AppLocalizationsAr extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return 'الإعداد المسبق: $presetName';
|
return 'الإعداد المسبق: $presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return 'يبدأ خلال $minutes دقيقة';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => 'تعديل الجهاز';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => 'اسم الجهاز';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => 'مثال: مكبر صوت غرفة المعيشة';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => 'حفظ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => 'متصل';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1254,4 +1254,24 @@ class AppLocalizationsBn extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return 'প্রিসেট: $presetName';
|
return 'প্রিসেট: $presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return '$minutes মিনিটে শুরু হবে';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => 'ডিভাইস সম্পাদনা করুন';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => 'ডিভাইসের নাম';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => 'যেমন: লিভিং রুমের স্পিকার';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => 'সংরক্ষণ করুন';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => 'সংযুক্ত';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1264,4 +1264,24 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return 'Preset: $presetName';
|
return 'Preset: $presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return 'Startet in $minutes Min.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => 'Gerät bearbeiten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => 'Gerätename';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => 'z. B. Wohnzimmer-Lautsprecher';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => 'Speichern';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => 'Verbunden';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1250,4 +1250,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return 'Preset: $presetName';
|
return 'Preset: $presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return 'Starts in $minutes min';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => 'Edit device';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => 'Device name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => 'e.g. Living Room Speaker';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => 'Save';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => 'Connected';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1259,4 +1259,24 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return 'Preset: $presetName';
|
return 'Preset: $presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return 'Empieza en $minutes min';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => 'Editar dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => 'Nombre del dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => 'Ej: Altavoz del living';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => 'Guardar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => 'Conectado';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1269,4 +1269,24 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return 'Preset : $presetName';
|
return 'Preset : $presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return 'Démarre dans $minutes min';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => 'Modifier l\'appareil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => 'Nom de l\'appareil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => 'ex. : Enceinte salon';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => 'Enregistrer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => 'Connecté';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1253,4 +1253,24 @@ class AppLocalizationsHi extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return 'प्रीसेट: $presetName';
|
return 'प्रीसेट: $presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return '$minutes मिनट में शुरू होगा';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => 'डिवाइस संपादित करें';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => 'डिवाइस का नाम';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => 'उदा. लिविंग रूम स्पीकर';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => 'सहेजें';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => 'कनेक्टेड';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1258,4 +1258,24 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return 'Preset: $presetName';
|
return 'Preset: $presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return 'Mulai dalam $minutes menit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => 'Edit perangkat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => 'Nama perangkat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => 'Mis: Speaker ruang tamu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => 'Simpan';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => 'Terhubung';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1264,4 +1264,24 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return 'Preset: $presetName';
|
return 'Preset: $presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return 'Inizia tra $minutes min';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => 'Modifica dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => 'Nome del dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => 'Es: Cassa del salotto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => 'Salva';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => 'Connesso';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1214,4 +1214,24 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return 'プリセット: $presetName';
|
return 'プリセット: $presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return '$minutes分後に開始';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => 'デバイスを編集';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => 'デバイス名';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => '例:リビングのスピーカー';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => '保存';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => '接続中';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1256,4 +1256,24 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return 'Preset: $presetName';
|
return 'Preset: $presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return 'Começa em $minutes min';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => 'Editar dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => 'Nome do dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => 'Ex: Caixa da sala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => 'Salvar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => 'Conectado';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1260,4 +1260,24 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return 'Пресет: $presetName';
|
return 'Пресет: $presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return 'Начнётся через $minutes мин';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => 'Изменить устройство';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => 'Название устройства';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => 'Напр.: Колонка в гостиной';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => 'Сохранить';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => 'Подключено';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1207,4 +1207,24 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String advancedEqDevicePresetLabel(Object presetName) {
|
String advancedEqDevicePresetLabel(Object presetName) {
|
||||||
return '预设:$presetName';
|
return '预设:$presetName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String preNoticeCountdown(int minutes) {
|
||||||
|
return '$minutes分钟后开始';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceEditTitle => '编辑设备';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameLabel => '设备名称';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameHint => '例:客厅音箱';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceNameConfirm => '保存';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get eqDeviceConnected => '已连接';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import '../l10n/display_names.dart';
|
|||||||
import '../l10n/gen/app_localizations.dart';
|
import '../l10n/gen/app_localizations.dart';
|
||||||
import '../modelos/emisora.dart';
|
import '../modelos/emisora.dart';
|
||||||
import '../modelos/grupo_favoritos.dart';
|
import '../modelos/grupo_favoritos.dart';
|
||||||
|
import '../modelos/preset_ecualizador.dart';
|
||||||
import '../widgets/ecualizador_widget.dart';
|
import '../widgets/ecualizador_widget.dart';
|
||||||
import '../widgets/pluri_glass_surface.dart';
|
import '../widgets/pluri_glass_surface.dart';
|
||||||
import '../widgets/pluri_icon.dart';
|
import '../widgets/pluri_icon.dart';
|
||||||
@@ -739,33 +740,9 @@ class _SeccionEcualizadorAvanzado extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
for (final entry in presetsDispositivo.entries)
|
for (final entry in presetsDispositivo.entries)
|
||||||
Padding(
|
_FilaDispositivo(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
deviceId: entry.key,
|
||||||
child: Row(
|
preset: entry.value,
|
||||||
children: [
|
|
||||||
const Icon(Icons.headphones_rounded, size: 20),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
entry.key,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
l10n.advancedEqDevicePresetLabel(
|
|
||||||
entry.value.nombre,
|
|
||||||
),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -774,6 +751,162 @@ class _SeccionEcualizadorAvanzado extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single device row in the known-devices list.
|
||||||
|
///
|
||||||
|
/// Shows a connection indicator (green dot) when [deviceId] matches the
|
||||||
|
/// currently active device. Tapping the edit icon opens [_DialogoEdicionDispositivo].
|
||||||
|
class _FilaDispositivo extends StatelessWidget {
|
||||||
|
const _FilaDispositivo({required this.deviceId, required this.preset});
|
||||||
|
|
||||||
|
final String deviceId;
|
||||||
|
final PresetEcualizador preset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final eq = context.watch<EstadoEcualizador>();
|
||||||
|
final isActive = eq.dispositivoActualId == deviceId;
|
||||||
|
final displayName = eq.nombreVisible(deviceId, '');
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (isActive)
|
||||||
|
const Icon(Icons.circle, size: 10, color: Colors.green)
|
||||||
|
else
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Icon(Icons.headphones_rounded, size: 20),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
displayName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
l10n.advancedEqDevicePresetLabel(preset.nombre),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit_rounded, size: 20),
|
||||||
|
tooltip: l10n.eqDeviceEditTitle,
|
||||||
|
onPressed: () => _abrirModal(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _abrirModal(BuildContext context) async {
|
||||||
|
await showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
showDragHandle: true,
|
||||||
|
builder: (ctx) => _DialogoEdicionDispositivo(
|
||||||
|
deviceId: deviceId,
|
||||||
|
preset: preset,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bottom sheet for editing a device's custom name and EQ preset.
|
||||||
|
class _DialogoEdicionDispositivo extends StatefulWidget {
|
||||||
|
const _DialogoEdicionDispositivo({
|
||||||
|
required this.deviceId,
|
||||||
|
required this.preset,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String deviceId;
|
||||||
|
final PresetEcualizador preset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DialogoEdicionDispositivo> createState() =>
|
||||||
|
_DialogoEdicionDispositivoState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogoEdicionDispositivoState
|
||||||
|
extends State<_DialogoEdicionDispositivo> {
|
||||||
|
late final TextEditingController _nombreCtrl;
|
||||||
|
late PresetEcualizador _presetActual;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final eq = context.read<EstadoEcualizador>();
|
||||||
|
final displayName = eq.nombreVisible(widget.deviceId, '');
|
||||||
|
_nombreCtrl = TextEditingController(text: displayName);
|
||||||
|
_presetActual = widget.preset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nombreCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _guardar() async {
|
||||||
|
final eq = context.read<EstadoEcualizador>();
|
||||||
|
await eq.renombrarDispositivo(widget.deviceId, _nombreCtrl.text);
|
||||||
|
if (_presetActual != widget.preset) {
|
||||||
|
await eq.guardarPresetDispositivo(widget.deviceId, _presetActual);
|
||||||
|
}
|
||||||
|
if (mounted) Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final bottom = MediaQuery.viewInsetsOf(context).bottom;
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(20, 0, 20, bottom + 24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.eqDeviceEditTitle,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _nombreCtrl,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: l10n.eqDeviceNameLabel,
|
||||||
|
hintText: l10n.eqDeviceNameHint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
EcualizadorWidget(
|
||||||
|
preset: _presetActual,
|
||||||
|
onCambio: (p) => setState(() => _presetActual = p),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: _guardar,
|
||||||
|
icon: const Icon(Icons.save_rounded),
|
||||||
|
label: Text(l10n.eqDeviceNameConfirm),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SeccionOrdenListas extends StatelessWidget {
|
class _SeccionOrdenListas extends StatelessWidget {
|
||||||
const _SeccionOrdenListas();
|
const _SeccionOrdenListas();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -153,11 +154,10 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
|||||||
Future<void> _detener() async {
|
Future<void> _detener() async {
|
||||||
final radio = context.read<EstadoRadio>();
|
final radio = context.read<EstadoRadio>();
|
||||||
final alarmas = context.read<EstadoAlarmas>();
|
final alarmas = context.read<EstadoAlarmas>();
|
||||||
final navigator = Navigator.of(context);
|
|
||||||
await _liberarAudioLocal();
|
await _liberarAudioLocal();
|
||||||
await radio.audio.pausar();
|
await radio.audio.pausar();
|
||||||
await alarmas.finalizarEjecucion(widget.alarma.id);
|
await alarmas.finalizarEjecucion(widget.alarma.id);
|
||||||
if (mounted) navigator.pop();
|
if (mounted) _dismissScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flutter-first snooze (S2-R1): tears down local audio, then routes
|
/// Flutter-first snooze (S2-R1): tears down local audio, then routes
|
||||||
@@ -166,11 +166,25 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
|||||||
Future<void> _posponer(int minutos) async {
|
Future<void> _posponer(int minutos) async {
|
||||||
final radio = context.read<EstadoRadio>();
|
final radio = context.read<EstadoRadio>();
|
||||||
final alarmas = context.read<EstadoAlarmas>();
|
final alarmas = context.read<EstadoAlarmas>();
|
||||||
final navigator = Navigator.of(context);
|
|
||||||
await _liberarAudioLocal();
|
await _liberarAudioLocal();
|
||||||
await radio.audio.pausar();
|
await radio.audio.pausar();
|
||||||
await alarmas.posponerAlarma(widget.alarma, minutos);
|
await alarmas.posponerAlarma(widget.alarma, minutos);
|
||||||
if (mounted) navigator.pop();
|
if (mounted) _dismissScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dismisses the alarm screen safely in both live-app and dead-app states.
|
||||||
|
///
|
||||||
|
/// When the alarm screen is the root activity (launched via full-screen intent
|
||||||
|
/// from a dead app), [Navigator.canPop] returns false and calling
|
||||||
|
/// [Navigator.pop] would be a no-op. In that case [SystemNavigator.pop] is
|
||||||
|
/// used to call `Activity.finish()` and return to the home screen.
|
||||||
|
void _dismissScreen() {
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
|
if (navigator.canPop()) {
|
||||||
|
navigator.pop();
|
||||||
|
} else {
|
||||||
|
SystemNavigator.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<int> _opcionesSnooze() {
|
List<int> _opcionesSnooze() {
|
||||||
|
|||||||
@@ -170,6 +170,19 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
|||||||
_l10n = l10n;
|
_l10n = l10n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a pre-notice template string with a literal `{minutes}` placeholder
|
||||||
|
/// for Kotlin to replace at broadcast-receiver fire time.
|
||||||
|
///
|
||||||
|
/// Strategy: call [preNoticeCountdown] with a unique sentinel integer and
|
||||||
|
/// replace the sentinel's string representation with `{minutes}`.
|
||||||
|
static String _preNoticeTemplate(AppLocalizations l10n) {
|
||||||
|
const sentinel = 42424242;
|
||||||
|
return l10n.preNoticeCountdown(sentinel).replaceFirst(
|
||||||
|
sentinel.toString(),
|
||||||
|
'{minutes}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<EventoAlarmaAndroid> get eventosAlarma => _eventosController.stream;
|
Stream<EventoAlarmaAndroid> get eventosAlarma => _eventosController.stream;
|
||||||
|
|
||||||
@@ -189,6 +202,7 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
|||||||
final programada = await _channel.invokeMethod<bool>('scheduleAlarm', {
|
final programada = await _channel.invokeMethod<bool>('scheduleAlarm', {
|
||||||
'id': alarma.id,
|
'id': alarma.id,
|
||||||
'title': localizedAlarmName(_textos, alarma.nombre),
|
'title': localizedAlarmName(_textos, alarma.nombre),
|
||||||
|
'preNoticeTemplate': _preNoticeTemplate(_textos),
|
||||||
'triggerAtMillis': proxima.millisecondsSinceEpoch,
|
'triggerAtMillis': proxima.millisecondsSinceEpoch,
|
||||||
'preNoticeAtMillis':
|
'preNoticeAtMillis':
|
||||||
alarma.snoozeHasta == null
|
alarma.snoozeHasta == null
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class ConfiguracionEcualizador {
|
|||||||
this.eqMultiDeviceEnabled = false,
|
this.eqMultiDeviceEnabled = false,
|
||||||
this.presetsDispositivo = const {},
|
this.presetsDispositivo = const {},
|
||||||
this.presetsMatriz = const {},
|
this.presetsMatriz = const {},
|
||||||
|
this.nombresDispositivos = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
final PresetEcualizador principal;
|
final PresetEcualizador principal;
|
||||||
@@ -26,6 +27,9 @@ class ConfiguracionEcualizador {
|
|||||||
|
|
||||||
/// Matrix presets: "stationUuid:deviceId" → PresetEcualizador.
|
/// Matrix presets: "stationUuid:deviceId" → PresetEcualizador.
|
||||||
final Map<String, PresetEcualizador> presetsMatriz;
|
final Map<String, PresetEcualizador> presetsMatriz;
|
||||||
|
|
||||||
|
/// Custom display names for devices: deviceId → custom name.
|
||||||
|
final Map<String, String> nombresDispositivos;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServicioEcualizador {
|
class ServicioEcualizador {
|
||||||
@@ -37,6 +41,7 @@ class ServicioEcualizador {
|
|||||||
static const _keyMultiDeviceEnabled = 'eq_multi_device_enabled_v1';
|
static const _keyMultiDeviceEnabled = 'eq_multi_device_enabled_v1';
|
||||||
static const _keyPresetsPorDispositivo = 'eq_preset_por_dispositivo_v1';
|
static const _keyPresetsPorDispositivo = 'eq_preset_por_dispositivo_v1';
|
||||||
static const _keyPresetsMatriz = 'eq_presets_matriz_v1';
|
static const _keyPresetsMatriz = 'eq_presets_matriz_v1';
|
||||||
|
static const _keyNombresDispositivos = 'eq_nombres_dispositivos_v1';
|
||||||
|
|
||||||
final SharedPreferences? _prefs;
|
final SharedPreferences? _prefs;
|
||||||
|
|
||||||
@@ -50,6 +55,7 @@ class ServicioEcualizador {
|
|||||||
final porEmisora = _leerPresetsPorEmisora(prefs);
|
final porEmisora = _leerPresetsPorEmisora(prefs);
|
||||||
final presetsDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo);
|
final presetsDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo);
|
||||||
final presetsMatriz = _leerMapa(prefs, _keyPresetsMatriz);
|
final presetsMatriz = _leerMapa(prefs, _keyPresetsMatriz);
|
||||||
|
final nombresDispositivos = _leerMapaStrings(prefs, _keyNombresDispositivos);
|
||||||
return ConfiguracionEcualizador(
|
return ConfiguracionEcualizador(
|
||||||
principal: principal,
|
principal: principal,
|
||||||
porEmisora: porEmisora,
|
porEmisora: porEmisora,
|
||||||
@@ -57,6 +63,7 @@ class ServicioEcualizador {
|
|||||||
eqMultiDeviceEnabled: prefs.getBool(_keyMultiDeviceEnabled) ?? false,
|
eqMultiDeviceEnabled: prefs.getBool(_keyMultiDeviceEnabled) ?? false,
|
||||||
presetsDispositivo: presetsDispositivo,
|
presetsDispositivo: presetsDispositivo,
|
||||||
presetsMatriz: presetsMatriz,
|
presetsMatriz: presetsMatriz,
|
||||||
|
nombresDispositivos: nombresDispositivos,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +106,11 @@ class ServicioEcualizador {
|
|||||||
config.presetsDispositivo,
|
config.presetsDispositivo,
|
||||||
);
|
);
|
||||||
await _guardarMapa(prefs, _keyPresetsMatriz, config.presetsMatriz);
|
await _guardarMapa(prefs, _keyPresetsMatriz, config.presetsMatriz);
|
||||||
|
await _guardarMapaStrings(
|
||||||
|
prefs,
|
||||||
|
_keyNombresDispositivos,
|
||||||
|
config.nombresDispositivos,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persists the multi-device feature toggle.
|
/// Persists the multi-device feature toggle.
|
||||||
@@ -145,6 +157,18 @@ class ServicioEcualizador {
|
|||||||
await _guardarMapa(prefs, _keyPresetsMatriz, mapa);
|
await _guardarMapa(prefs, _keyPresetsMatriz, mapa);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the persisted device custom names map.
|
||||||
|
Future<Map<String, String>> cargarNombresDispositivos() async {
|
||||||
|
final prefs = await _resolverPrefs();
|
||||||
|
return _leerMapaStrings(prefs, _keyNombresDispositivos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persists the device custom names map.
|
||||||
|
Future<void> guardarNombresDispositivos(Map<String, String> nombres) async {
|
||||||
|
final prefs = await _resolverPrefs();
|
||||||
|
await _guardarMapaStrings(prefs, _keyNombresDispositivos, nombres);
|
||||||
|
}
|
||||||
|
|
||||||
/// Reads a `Map<String, PresetEcualizador>` from a SharedPreferences JSON key.
|
/// Reads a `Map<String, PresetEcualizador>` from a SharedPreferences JSON key.
|
||||||
Map<String, PresetEcualizador> _leerMapa(
|
Map<String, PresetEcualizador> _leerMapa(
|
||||||
SharedPreferences prefs,
|
SharedPreferences prefs,
|
||||||
@@ -174,6 +198,26 @@ class ServicioEcualizador {
|
|||||||
await prefs.setString(key, jsonEncode(serializado));
|
await prefs.setString(key, jsonEncode(serializado));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads a `Map<String, String>` from a SharedPreferences JSON key.
|
||||||
|
Map<String, String> _leerMapaStrings(SharedPreferences prefs, String key) {
|
||||||
|
final raw = prefs.getString(key);
|
||||||
|
if (raw == null || raw.isEmpty) return {};
|
||||||
|
try {
|
||||||
|
final data = Map<String, dynamic>.from(jsonDecode(raw) as Map);
|
||||||
|
return data.map((k, v) => MapEntry(k, v as String));
|
||||||
|
} catch (_) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _guardarMapaStrings(
|
||||||
|
SharedPreferences prefs,
|
||||||
|
String key,
|
||||||
|
Map<String, String> mapa,
|
||||||
|
) async {
|
||||||
|
await prefs.setString(key, jsonEncode(mapa));
|
||||||
|
}
|
||||||
|
|
||||||
PresetEcualizador _leerPresetPrincipal(SharedPreferences prefs) {
|
PresetEcualizador _leerPresetPrincipal(SharedPreferences prefs) {
|
||||||
final raw = prefs.getString(_keyPresetPrincipal);
|
final raw = prefs.getString(_keyPresetPrincipal);
|
||||||
if (raw == null || raw.isEmpty) {
|
if (raw == null || raw.isEmpty) {
|
||||||
|
|||||||
@@ -538,4 +538,408 @@ void main() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// eq-device-autoswitch-ux Phase 2: startup seeding + rename API
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
group('EstadoEcualizador — startup device seeding (eq-device-autoswitch-ux Phase 2)', () {
|
||||||
|
const btDevice = DispositivoAudio(
|
||||||
|
id: 'bt_a2dp:AA:BB',
|
||||||
|
tipo: TipoDispositivo.bluetoothA2dp,
|
||||||
|
nombre: 'BT Speaker',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2.1 RED — after cargarPersistido with multiDevice ON, dispositivoActualId is seeded
|
||||||
|
test('2.1 cargarPersistido seeds dispositivoActualId from obtenerDispositivoActual', () async {
|
||||||
|
final fakeDispositivo = FakeServicioDispositivoAudio()
|
||||||
|
..emitirDispositivo(btDevice); // sets _dispositivoActual
|
||||||
|
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||||
|
dispositivoAudio: fakeDispositivo,
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
|
||||||
|
expect(eq.dispositivoActualId, equals('bt_a2dp:AA:BB'));
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2.2 RED — first-seen device is bootstrapped from resolved preset at startup
|
||||||
|
test('2.2 first-seen device is bootstrapped at startup with resolved preset', () async {
|
||||||
|
final fakeServicio = FakeServicioEcualizador(
|
||||||
|
principal: PresetEcualizador.rock,
|
||||||
|
eqMultiDeviceEnabled: true,
|
||||||
|
presetsDispositivo: {},
|
||||||
|
);
|
||||||
|
final fakeDispositivo = FakeServicioDispositivoAudio()
|
||||||
|
..emitirDispositivo(btDevice);
|
||||||
|
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: fakeServicio,
|
||||||
|
dispositivoAudio: fakeDispositivo,
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
eq.presetsDispositivo[btDevice.id],
|
||||||
|
equals(PresetEcualizador.rock),
|
||||||
|
);
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2.3 RED — obtenerDispositivoActual throws → fallback to 'builtin_speaker'
|
||||||
|
test('2.3 obtenerDispositivoActual throws → dispositivoActualId falls back to builtin_speaker', () async {
|
||||||
|
final fakeDispositivo = FakeServicioDispositivoAudioThrows();
|
||||||
|
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||||
|
dispositivoAudio: fakeDispositivo,
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido(); // must not throw
|
||||||
|
|
||||||
|
expect(eq.dispositivoActualId, equals('builtin_speaker'));
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2.4 RED — toggle off → obtenerDispositivoActual is never called
|
||||||
|
test('2.4 eqMultiDeviceEnabled=false → obtenerDispositivoActual NOT called', () async {
|
||||||
|
final fakeDispositivo = FakeServicioDispositivoAudioThrows();
|
||||||
|
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: false),
|
||||||
|
dispositivoAudio: fakeDispositivo,
|
||||||
|
);
|
||||||
|
// If obtenerDispositivoActual were called it would throw → test would fail
|
||||||
|
await expectLater(eq.cargarPersistido(), completes);
|
||||||
|
expect(eq.dispositivoActualId, isNull);
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('EstadoEcualizador — rename API (eq-device-autoswitch-ux Phase 2)', () {
|
||||||
|
// 2.6 RED — renombrarDispositivo writes to _nombresDispositivos and notifies
|
||||||
|
test('2.6a renombrarDispositivo stores name and notifyListeners', () async {
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||||
|
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
|
||||||
|
var avisos = 0;
|
||||||
|
eq.addListener(() => avisos++);
|
||||||
|
await eq.renombrarDispositivo('bt_a2dp:AA:BB', 'Living Room BT');
|
||||||
|
|
||||||
|
expect(eq.obtenerNombreDispositivo('bt_a2dp:AA:BB'), equals('Living Room BT'));
|
||||||
|
expect(avisos, greaterThanOrEqualTo(1));
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2.6 RED — empty name is no-op (preserves existing name)
|
||||||
|
test('2.6b renombrarDispositivo with empty string is no-op', () async {
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||||
|
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
await eq.renombrarDispositivo('bt_a2dp:AA:BB', 'My Headset');
|
||||||
|
await eq.renombrarDispositivo('bt_a2dp:AA:BB', '');
|
||||||
|
|
||||||
|
expect(eq.obtenerNombreDispositivo('bt_a2dp:AA:BB'), equals('My Headset'));
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2.7 RED — obtenerNombreDispositivo returns empty string for unknown device
|
||||||
|
test('2.7 obtenerNombreDispositivo returns empty string for unknown device', () async {
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(),
|
||||||
|
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
expect(eq.obtenerNombreDispositivo('unknown_device'), equals(''));
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2.8 RED — nombreVisible fallback chain: custom > platform > id
|
||||||
|
test('2.8a nombreVisible returns custom name when available', () async {
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||||
|
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
await eq.renombrarDispositivo('bt_a2dp:AA:BB', 'Studio Monitors');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
eq.nombreVisible('bt_a2dp:AA:BB', 'USB Audio'),
|
||||||
|
equals('Studio Monitors'),
|
||||||
|
);
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('2.8b nombreVisible returns platform name when no custom name', () async {
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(),
|
||||||
|
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
eq.nombreVisible('bt_a2dp:AA:BB', 'Sony WH-1000XM5'),
|
||||||
|
equals('Sony WH-1000XM5'),
|
||||||
|
);
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('2.8c nombreVisible returns raw deviceId when no custom and no platform name', () async {
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(),
|
||||||
|
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
|
||||||
|
expect(eq.nombreVisible('bt_a2dp:AA:BB', ''), equals('bt_a2dp:AA:BB'));
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
// nombresDispositivos getter
|
||||||
|
test('2.9 nombresDispositivos getter returns current map', () async {
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||||
|
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
await eq.renombrarDispositivo('bt_a2dp:AA:BB', 'My BT');
|
||||||
|
|
||||||
|
expect(eq.nombresDispositivos['bt_a2dp:AA:BB'], equals('My BT'));
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2.10 RED — _nombresDispositivos loaded from ConfiguracionEcualizador.nombresDispositivos
|
||||||
|
test('2.10 cargarPersistido loads nombresDispositivos from config', () async {
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(
|
||||||
|
nombresDispositivos: {'bt_a2dp:AA:BB': 'Loaded Name'},
|
||||||
|
),
|
||||||
|
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
|
||||||
|
expect(eq.obtenerNombreDispositivo('bt_a2dp:AA:BB'), equals('Loaded Name'));
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CRITICAL-1: renombrarDispositivo is a no-op when toggle is OFF
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
group('EstadoEcualizador — renombrarDispositivo toggle guard (CRITICAL-1)', () {
|
||||||
|
test(
|
||||||
|
'renombrarDispositivo is no-op when eqMultiDeviceEnabled is false',
|
||||||
|
() async {
|
||||||
|
final fakeServicio = FakeServicioEcualizador(eqMultiDeviceEnabled: false);
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: fakeServicio,
|
||||||
|
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
|
||||||
|
var avisos = 0;
|
||||||
|
eq.addListener(() => avisos++);
|
||||||
|
await eq.renombrarDispositivo('bt_a2dp:AA:BB', 'My Device');
|
||||||
|
|
||||||
|
// No name stored, no listeners notified, service not called
|
||||||
|
expect(eq.obtenerNombreDispositivo('bt_a2dp:AA:BB'), equals(''));
|
||||||
|
expect(avisos, equals(0));
|
||||||
|
expect(fakeServicio.config.nombresDispositivos, isEmpty);
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'renombrarDispositivo works normally when eqMultiDeviceEnabled is true',
|
||||||
|
() async {
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||||
|
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
|
||||||
|
var avisos = 0;
|
||||||
|
eq.addListener(() => avisos++);
|
||||||
|
await eq.renombrarDispositivo('bt_a2dp:AA:BB', 'My Device');
|
||||||
|
|
||||||
|
expect(eq.obtenerNombreDispositivo('bt_a2dp:AA:BB'), equals('My Device'));
|
||||||
|
expect(avisos, greaterThanOrEqualTo(1));
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CRITICAL-2: guardarPresetDispositivo method
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
group('EstadoEcualizador — guardarPresetDispositivo (CRITICAL-2)', () {
|
||||||
|
const btDevice = DispositivoAudio(
|
||||||
|
id: 'bt_a2dp:AA:BB',
|
||||||
|
tipo: TipoDispositivo.bluetoothA2dp,
|
||||||
|
nombre: 'BT Speaker',
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'guardarPresetDispositivo stores preset and notifies listeners',
|
||||||
|
() async {
|
||||||
|
final fakeServicio = FakeServicioEcualizador(
|
||||||
|
eqMultiDeviceEnabled: true,
|
||||||
|
presetsDispositivo: {btDevice.id: PresetEcualizador.flat},
|
||||||
|
);
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: fakeServicio,
|
||||||
|
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
|
||||||
|
var avisos = 0;
|
||||||
|
eq.addListener(() => avisos++);
|
||||||
|
await eq.guardarPresetDispositivo(btDevice.id, PresetEcualizador.jazz);
|
||||||
|
|
||||||
|
expect(eq.presetsDispositivo[btDevice.id], equals(PresetEcualizador.jazz));
|
||||||
|
expect(fakeServicio.config.presetsDispositivo[btDevice.id], equals(PresetEcualizador.jazz));
|
||||||
|
expect(avisos, greaterThanOrEqualTo(1));
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'guardarPresetDispositivo is no-op when toggle is OFF',
|
||||||
|
() async {
|
||||||
|
final fakeServicio = FakeServicioEcualizador(
|
||||||
|
eqMultiDeviceEnabled: false,
|
||||||
|
presetsDispositivo: {btDevice.id: PresetEcualizador.flat},
|
||||||
|
);
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: fakeServicio,
|
||||||
|
dispositivoAudio: FakeServicioDispositivoAudio(),
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
|
||||||
|
var avisos = 0;
|
||||||
|
eq.addListener(() => avisos++);
|
||||||
|
await eq.guardarPresetDispositivo(btDevice.id, PresetEcualizador.jazz);
|
||||||
|
|
||||||
|
// Preset must remain unchanged
|
||||||
|
expect(eq.presetsDispositivo[btDevice.id], equals(PresetEcualizador.flat));
|
||||||
|
expect(avisos, equals(0));
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'guardarPresetDispositivo triggers re-resolve and apply when device is active',
|
||||||
|
() async {
|
||||||
|
final fakeAudio = FakeServicioAudio();
|
||||||
|
final fakeDispositivo = FakeServicioDispositivoAudio()
|
||||||
|
..emitirDispositivo(btDevice);
|
||||||
|
const stationUuid = 'station-save-test';
|
||||||
|
final fakeServicio = FakeServicioEcualizador(
|
||||||
|
eqMultiDeviceEnabled: true,
|
||||||
|
presetsDispositivo: {btDevice.id: PresetEcualizador.flat},
|
||||||
|
);
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: fakeAudio,
|
||||||
|
servicio: fakeServicio,
|
||||||
|
dispositivoAudio: fakeDispositivo,
|
||||||
|
emisoraActualUuid: () => stationUuid,
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
fakeAudio.presetsAplicados.clear();
|
||||||
|
await eq.guardarPresetDispositivo(btDevice.id, PresetEcualizador.rock);
|
||||||
|
|
||||||
|
// Audio was re-applied (re-resolve was triggered)
|
||||||
|
expect(fakeAudio.presetsAplicados, isNotEmpty);
|
||||||
|
// The resolved preset is the device preset (level 3, no station preset set)
|
||||||
|
expect(fakeAudio.presetsAplicados.last, equals(PresetEcualizador.rock));
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'guardarPresetDispositivo does NOT re-apply when device is not active',
|
||||||
|
() async {
|
||||||
|
final fakeAudio = FakeServicioAudio();
|
||||||
|
final fakeServicio = FakeServicioEcualizador(
|
||||||
|
eqMultiDeviceEnabled: true,
|
||||||
|
presetsDispositivo: {
|
||||||
|
btDevice.id: PresetEcualizador.flat,
|
||||||
|
'other_device': PresetEcualizador.flat,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final fakeDispositivo = FakeServicioDispositivoAudio()
|
||||||
|
..emitirDispositivo(btDevice);
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: fakeAudio,
|
||||||
|
servicio: fakeServicio,
|
||||||
|
dispositivoAudio: fakeDispositivo,
|
||||||
|
emisoraActualUuid: () => null,
|
||||||
|
);
|
||||||
|
await eq.cargarPersistido();
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
// Active device is btDevice; save for 'other_device'
|
||||||
|
fakeAudio.presetsAplicados.clear();
|
||||||
|
await eq.guardarPresetDispositivo('other_device', PresetEcualizador.rock);
|
||||||
|
|
||||||
|
// Preset saved but audio NOT re-applied since 'other_device' is not active
|
||||||
|
expect(eq.presetsDispositivo['other_device'], equals(PresetEcualizador.rock));
|
||||||
|
expect(fakeAudio.presetsAplicados, isEmpty);
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// WARNING-3: FakeServicioEcualizador preserves nombresDispositivos
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
group('FakeServicioEcualizador — nombresDispositivos preservation (WARNING-3)', () {
|
||||||
|
test(
|
||||||
|
'guardarPrincipal preserves nombresDispositivos',
|
||||||
|
() async {
|
||||||
|
final fake = FakeServicioEcualizador(
|
||||||
|
nombresDispositivos: {'dev1': 'My Speaker'},
|
||||||
|
);
|
||||||
|
await fake.guardarPrincipal(PresetEcualizador.jazz);
|
||||||
|
expect(fake.config.nombresDispositivos['dev1'], equals('My Speaker'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'guardarActivo preserves nombresDispositivos',
|
||||||
|
() async {
|
||||||
|
final fake = FakeServicioEcualizador(
|
||||||
|
nombresDispositivos: {'dev1': 'My Speaker'},
|
||||||
|
);
|
||||||
|
await fake.guardarActivo(false);
|
||||||
|
expect(fake.config.nombresDispositivos['dev1'], equals('My Speaker'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ class FakeServicioEcualizador extends ServicioEcualizador {
|
|||||||
bool eqMultiDeviceEnabled = false,
|
bool eqMultiDeviceEnabled = false,
|
||||||
Map<String, PresetEcualizador>? presetsDispositivo,
|
Map<String, PresetEcualizador>? presetsDispositivo,
|
||||||
Map<String, PresetEcualizador>? presetsMatriz,
|
Map<String, PresetEcualizador>? presetsMatriz,
|
||||||
|
Map<String, String>? nombresDispositivos,
|
||||||
}) : _config = ConfiguracionEcualizador(
|
}) : _config = ConfiguracionEcualizador(
|
||||||
principal: principal ?? PresetEcualizador.flat,
|
principal: principal ?? PresetEcualizador.flat,
|
||||||
porEmisora: porEmisora ?? {},
|
porEmisora: porEmisora ?? {},
|
||||||
@@ -294,6 +295,7 @@ class FakeServicioEcualizador extends ServicioEcualizador {
|
|||||||
eqMultiDeviceEnabled: eqMultiDeviceEnabled,
|
eqMultiDeviceEnabled: eqMultiDeviceEnabled,
|
||||||
presetsDispositivo: presetsDispositivo ?? {},
|
presetsDispositivo: presetsDispositivo ?? {},
|
||||||
presetsMatriz: presetsMatriz ?? {},
|
presetsMatriz: presetsMatriz ?? {},
|
||||||
|
nombresDispositivos: nombresDispositivos ?? {},
|
||||||
);
|
);
|
||||||
|
|
||||||
ConfiguracionEcualizador _config;
|
ConfiguracionEcualizador _config;
|
||||||
@@ -311,6 +313,7 @@ class FakeServicioEcualizador extends ServicioEcualizador {
|
|||||||
eqMultiDeviceEnabled: _config.eqMultiDeviceEnabled,
|
eqMultiDeviceEnabled: _config.eqMultiDeviceEnabled,
|
||||||
presetsDispositivo: _config.presetsDispositivo,
|
presetsDispositivo: _config.presetsDispositivo,
|
||||||
presetsMatriz: _config.presetsMatriz,
|
presetsMatriz: _config.presetsMatriz,
|
||||||
|
nombresDispositivos: _config.nombresDispositivos,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +326,7 @@ class FakeServicioEcualizador extends ServicioEcualizador {
|
|||||||
eqMultiDeviceEnabled: _config.eqMultiDeviceEnabled,
|
eqMultiDeviceEnabled: _config.eqMultiDeviceEnabled,
|
||||||
presetsDispositivo: _config.presetsDispositivo,
|
presetsDispositivo: _config.presetsDispositivo,
|
||||||
presetsMatriz: _config.presetsMatriz,
|
presetsMatriz: _config.presetsMatriz,
|
||||||
|
nombresDispositivos: _config.nombresDispositivos,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,8 +403,45 @@ class FakeServicioEcualizador extends ServicioEcualizador {
|
|||||||
eqMultiDeviceEnabled: _config.eqMultiDeviceEnabled,
|
eqMultiDeviceEnabled: _config.eqMultiDeviceEnabled,
|
||||||
presetsDispositivo: _config.presetsDispositivo,
|
presetsDispositivo: _config.presetsDispositivo,
|
||||||
presetsMatriz: mapa,
|
presetsMatriz: mapa,
|
||||||
|
nombresDispositivos: _config.nombresDispositivos,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> guardarNombresDispositivos(Map<String, String> nombres) async {
|
||||||
|
_config = ConfiguracionEcualizador(
|
||||||
|
principal: _config.principal,
|
||||||
|
porEmisora: _config.porEmisora,
|
||||||
|
activo: _config.activo,
|
||||||
|
eqMultiDeviceEnabled: _config.eqMultiDeviceEnabled,
|
||||||
|
presetsDispositivo: _config.presetsDispositivo,
|
||||||
|
presetsMatriz: _config.presetsMatriz,
|
||||||
|
nombresDispositivos: nombres,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [ServicioDispositivoAudio] fake that throws on [obtenerDispositivoActual].
|
||||||
|
///
|
||||||
|
/// Used to test the graceful-failure path in [EstadoEcualizador.cargarPersistido].
|
||||||
|
class FakeServicioDispositivoAudioThrows extends ServicioDispositivoAudio {
|
||||||
|
final _controller = StreamController<DispositivoAudio>.broadcast();
|
||||||
|
|
||||||
|
@override
|
||||||
|
DispositivoAudio? get dispositivoActual => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<DispositivoAudio> get onDispositivoCambiado => _controller.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DispositivoAudio> obtenerDispositivoActual() async {
|
||||||
|
throw Exception('Platform channel error: device unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await _controller.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fake implementation of [ServicioDispositivoAudio] for unit tests.
|
/// Fake implementation of [ServicioDispositivoAudio] for unit tests.
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('preNoticeCountdown ARB key', () {
|
||||||
|
test('English returns expected sentence with minutes placeholder', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('en'));
|
||||||
|
expect(l10n.preNoticeCountdown(30), 'Starts in 30 min');
|
||||||
|
expect(l10n.preNoticeCountdown(1), 'Starts in 1 min');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Spanish returns localized sentence', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('es'));
|
||||||
|
expect(l10n.preNoticeCountdown(30), 'Empieza en 30 min');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Arabic returns localized sentence', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('ar'));
|
||||||
|
expect(l10n.preNoticeCountdown(5), 'يبدأ خلال 5 دقيقة');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('German returns localized sentence', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('de'));
|
||||||
|
expect(l10n.preNoticeCountdown(10), 'Startet in 10 Min.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('French returns localized sentence', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('fr'));
|
||||||
|
expect(l10n.preNoticeCountdown(15), 'Démarre dans 15 min');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Portuguese returns localized sentence', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('pt'));
|
||||||
|
expect(l10n.preNoticeCountdown(20), 'Começa em 20 min');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Italian returns localized sentence', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('it'));
|
||||||
|
expect(l10n.preNoticeCountdown(25), 'Inizia tra 25 min');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Japanese returns localized sentence', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('ja'));
|
||||||
|
expect(l10n.preNoticeCountdown(30), '30分後に開始');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Russian returns localized sentence', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('ru'));
|
||||||
|
expect(l10n.preNoticeCountdown(30), 'Начнётся через 30 мин');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Chinese returns localized sentence', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('zh'));
|
||||||
|
expect(l10n.preNoticeCountdown(30), '30分钟后开始');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Hindi returns localized sentence', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('hi'));
|
||||||
|
expect(l10n.preNoticeCountdown(30), '30 मिनट में शुरू होगा');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Bengali returns localized sentence', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('bn'));
|
||||||
|
expect(l10n.preNoticeCountdown(30), '30 মিনিটে শুরু হবে');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Indonesian returns localized sentence', () {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('id'));
|
||||||
|
expect(l10n.preNoticeCountdown(30), 'Mulai dalam 30 menit');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import 'package:pluriwave/estado/estado_grabacion.dart';
|
|||||||
import 'package:pluriwave/estado/estado_idioma.dart';
|
import 'package:pluriwave/estado/estado_idioma.dart';
|
||||||
import 'package:pluriwave/estado/estado_radio.dart';
|
import 'package:pluriwave/estado/estado_radio.dart';
|
||||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||||
|
import 'package:pluriwave/modelos/dispositivo_audio.dart';
|
||||||
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||||
import 'package:pluriwave/pantallas/pantalla_ajustes.dart';
|
import 'package:pluriwave/pantallas/pantalla_ajustes.dart';
|
||||||
import 'package:pluriwave/servicios/servicio_grabacion_radio.dart';
|
import 'package:pluriwave/servicios/servicio_grabacion_radio.dart';
|
||||||
@@ -92,6 +93,42 @@ void main() {
|
|||||||
await tester.pumpAndSettle(const Duration(milliseconds: 100));
|
await tester.pumpAndSettle(const Duration(milliseconds: 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also update crearEstado to support nombresDispositivos
|
||||||
|
Future<EstadoRadio> crearEstadoConNombres({
|
||||||
|
bool eqMultiDeviceEnabled = true,
|
||||||
|
Map<String, PresetEcualizador>? presetsDispositivo,
|
||||||
|
Map<String, String>? nombresDispositivos,
|
||||||
|
String? activeDeviceId,
|
||||||
|
}) async {
|
||||||
|
final fakeDispositivo = activeDeviceId != null
|
||||||
|
? (FakeServicioDispositivoAudio()
|
||||||
|
..emitirDispositivo(
|
||||||
|
DispositivoAudio(
|
||||||
|
id: activeDeviceId,
|
||||||
|
tipo: TipoDispositivo.bluetoothA2dp,
|
||||||
|
nombre: 'BT Speaker',
|
||||||
|
),
|
||||||
|
))
|
||||||
|
: null;
|
||||||
|
final estado = EstadoRadio(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
favoritos: FakeServicioFavoritos(),
|
||||||
|
radio: FakeServicioRadio(),
|
||||||
|
servicioEcualizador: FakeServicioEcualizador(
|
||||||
|
eqMultiDeviceEnabled: eqMultiDeviceEnabled,
|
||||||
|
presetsDispositivo: presetsDispositivo ??
|
||||||
|
{'bt_a2dp:AA:BB': PresetEcualizador.rock},
|
||||||
|
nombresDispositivos: nombresDispositivos ?? {},
|
||||||
|
),
|
||||||
|
servicioGrabacion: _FakeGrabacion(),
|
||||||
|
resolverArchivoCustom: _archivoCustomVacio,
|
||||||
|
iniciarAutomaticamente: false,
|
||||||
|
dispositivoAudio: fakeDispositivo,
|
||||||
|
);
|
||||||
|
await estado.ecualizador.cargarPersistido();
|
||||||
|
return estado;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Phase 7 tests ──────────────────────────────────────────────────────────
|
// ── Phase 7 tests ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
group('_SeccionEcualizadorAvanzado (Phase 7)', () {
|
group('_SeccionEcualizadorAvanzado (Phase 7)', () {
|
||||||
@@ -194,6 +231,155 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Phase 3 (eq-device-autoswitch-ux): connection indicator + modal ──────────
|
||||||
|
|
||||||
|
group('SeccionEcualizadorAvanzado connection indicator Phase 3', () {
|
||||||
|
// 3.1 RED — active device row shows green connection dot
|
||||||
|
testWidgets('3.1 active device row shows green connection indicator', (tester) async {
|
||||||
|
setLargeSurface(tester);
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
const activeId = 'bt_a2dp:AA:BB';
|
||||||
|
final estado = await crearEstadoConNombres(
|
||||||
|
eqMultiDeviceEnabled: true,
|
||||||
|
presetsDispositivo: {
|
||||||
|
activeId: PresetEcualizador.rock,
|
||||||
|
'wired_headset': PresetEcualizador.jazz,
|
||||||
|
},
|
||||||
|
activeDeviceId: activeId,
|
||||||
|
);
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildAjustes(estado));
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
await tester.scrollUntilVisible(
|
||||||
|
find.text('Known audio devices'),
|
||||||
|
300,
|
||||||
|
scrollable: find.byType(Scrollable).first,
|
||||||
|
);
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
// Green connection dot should be present (Icon with green color for active device)
|
||||||
|
final greenIcons = tester.widgetList<Icon>(find.byType(Icon)).where(
|
||||||
|
(icon) => icon.color == Colors.green,
|
||||||
|
);
|
||||||
|
// At least one green icon present
|
||||||
|
expect(greenIcons, isNotEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3.2 RED — tapping device row opens bottom sheet with TextField and EcualizadorWidget
|
||||||
|
testWidgets('3.2 tapping device row opens modal with TextField', (tester) async {
|
||||||
|
setLargeSurface(tester);
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
final estado = await crearEstadoConNombres(
|
||||||
|
presetsDispositivo: {'bt_a2dp:AA:BB': PresetEcualizador.rock},
|
||||||
|
);
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildAjustes(estado));
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
await tester.scrollUntilVisible(
|
||||||
|
find.text('Known audio devices'),
|
||||||
|
300,
|
||||||
|
scrollable: find.byType(Scrollable).first,
|
||||||
|
);
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
// Tap the device row (tap the edit icon or the row itself)
|
||||||
|
final editIcons = find.byIcon(Icons.edit_rounded);
|
||||||
|
expect(editIcons, findsWidgets);
|
||||||
|
await tester.tap(editIcons.first);
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
// Bottom sheet should appear with a TextField
|
||||||
|
expect(find.byType(TextField), findsWidgets);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3.6 RED — renaming in modal and confirming calls renombrarDispositivo
|
||||||
|
testWidgets('3.6 confirming rename in modal updates device name', (tester) async {
|
||||||
|
setLargeSurface(tester);
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
const deviceId = 'bt_a2dp:AA:BB';
|
||||||
|
final estado = await crearEstadoConNombres(
|
||||||
|
presetsDispositivo: {deviceId: PresetEcualizador.rock},
|
||||||
|
);
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildAjustes(estado));
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
await tester.scrollUntilVisible(
|
||||||
|
find.text('Known audio devices'),
|
||||||
|
300,
|
||||||
|
scrollable: find.byType(Scrollable).first,
|
||||||
|
);
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
// Open modal
|
||||||
|
await tester.tap(find.byIcon(Icons.edit_rounded).first);
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
// Enter a new name
|
||||||
|
final textField = find.byType(TextField).first;
|
||||||
|
await tester.enterText(textField, 'My Living Room Speaker');
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
// Tap confirm/save button
|
||||||
|
final saveButton = find.byIcon(Icons.save_rounded);
|
||||||
|
expect(saveButton, findsWidgets);
|
||||||
|
await tester.tap(saveButton.first);
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
// Device should now have the new name
|
||||||
|
expect(
|
||||||
|
estado.ecualizador.obtenerNombreDispositivo(deviceId),
|
||||||
|
equals('My Living Room Speaker'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3.7 RED — dismissing modal without confirming leaves name unchanged
|
||||||
|
testWidgets('3.7 dismissing modal without confirming leaves name unchanged', (tester) async {
|
||||||
|
setLargeSurface(tester);
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
const deviceId = 'bt_a2dp:AA:BB';
|
||||||
|
final estado = await crearEstadoConNombres(
|
||||||
|
presetsDispositivo: {deviceId: PresetEcualizador.rock},
|
||||||
|
nombresDispositivos: {deviceId: 'Original Name'},
|
||||||
|
);
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildAjustes(estado));
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
await tester.scrollUntilVisible(
|
||||||
|
find.text('Known audio devices'),
|
||||||
|
300,
|
||||||
|
scrollable: find.byType(Scrollable).first,
|
||||||
|
);
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
// Open modal
|
||||||
|
await tester.tap(find.byIcon(Icons.edit_rounded).first);
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
// Change the text but do NOT confirm
|
||||||
|
final textField = find.byType(TextField).first;
|
||||||
|
await tester.enterText(textField, 'New Name Not Saved');
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
// Dismiss by pressing back/escape
|
||||||
|
await tester.tapAt(const Offset(100, 100)); // tap outside bottom sheet
|
||||||
|
await pumpStable(tester);
|
||||||
|
|
||||||
|
// Name should remain unchanged
|
||||||
|
expect(
|
||||||
|
estado.ecualizador.obtenerNombreDispositivo(deviceId),
|
||||||
|
equals('Original Name'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Infrastructure ──────────────────────────────────────────────────────────
|
// ── Infrastructure ──────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pluriwave/estado/estado_alarmas.dart';
|
||||||
|
import 'package:pluriwave/estado/estado_radio.dart';
|
||||||
|
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||||
|
import 'package:pluriwave/modelos/alarma_musical.dart';
|
||||||
|
import 'package:pluriwave/modelos/emisora.dart';
|
||||||
|
import 'package:pluriwave/pantallas/pantalla_alarma_sonando.dart';
|
||||||
|
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||||||
|
import 'package:pluriwave/servicios/servicio_audio.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../helpers/fakes.dart';
|
||||||
|
import '../helpers/fakes_alarmas.dart';
|
||||||
|
|
||||||
|
// Tracks SystemNavigator.pop() calls via the platform channel mock.
|
||||||
|
class _SystemNavigatorSpy {
|
||||||
|
int popCalls = 0;
|
||||||
|
|
||||||
|
void install() {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(
|
||||||
|
SystemChannels.platform,
|
||||||
|
(call) async {
|
||||||
|
if (call.method == 'SystemNavigator.pop') {
|
||||||
|
popCalls++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uninstall() {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(SystemChannels.platform, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _montarComoRaiz(
|
||||||
|
WidgetTester tester, {
|
||||||
|
required FakePuertoAlarmasAndroid android,
|
||||||
|
required EstadoAlarmas estadoAlarmas,
|
||||||
|
required EstadoRadio radio,
|
||||||
|
}) async {
|
||||||
|
tester.view.physicalSize = const Size(1440, 3200);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
|
||||||
|
// Mount alarm screen as ROOT route — simulates dead-app FSI launch.
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider<EstadoRadio>.value(value: radio),
|
||||||
|
ChangeNotifierProvider<EstadoAlarmas>.value(value: estadoAlarmas),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
locale: const Locale('es'),
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: PantallaAlarmaSonando(
|
||||||
|
alarma: estadoAlarmas.alarmas.single,
|
||||||
|
audioPrearrancado: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _montarConHistorial(
|
||||||
|
WidgetTester tester, {
|
||||||
|
required FakePuertoAlarmasAndroid android,
|
||||||
|
required EstadoAlarmas estadoAlarmas,
|
||||||
|
required EstadoRadio radio,
|
||||||
|
}) async {
|
||||||
|
tester.view.physicalSize = const Size(1440, 3200);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
|
||||||
|
// Mount with a previous route so canPop() returns true.
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider<EstadoRadio>.value(value: radio),
|
||||||
|
ChangeNotifierProvider<EstadoAlarmas>.value(value: estadoAlarmas),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
locale: const Locale('es'),
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final navigator = tester.state<NavigatorState>(find.byType(Navigator));
|
||||||
|
unawaited(
|
||||||
|
navigator.push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => PantallaAlarmaSonando(
|
||||||
|
alarma: estadoAlarmas.alarmas.single,
|
||||||
|
audioPrearrancado: true,
|
||||||
|
),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<_Env> _buildEnv() async {
|
||||||
|
final audio = FakeServicioAudio();
|
||||||
|
audio.emitirEstado(EstadoReproduccion.reproduciendo);
|
||||||
|
final radio = EstadoRadio(
|
||||||
|
audio: audio,
|
||||||
|
favoritos: FakeServicioFavoritos(),
|
||||||
|
radio: FakeServicioRadio(),
|
||||||
|
servicioEcualizador: FakeServicioEcualizador(),
|
||||||
|
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||||||
|
iniciarAutomaticamente: false,
|
||||||
|
);
|
||||||
|
final android = FakePuertoAlarmasAndroid();
|
||||||
|
final estadoAlarmas = EstadoAlarmas(
|
||||||
|
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 11, 7, 0)),
|
||||||
|
android: android,
|
||||||
|
iniciarAutomaticamente: false,
|
||||||
|
);
|
||||||
|
await estadoAlarmas.guardarAlarma(
|
||||||
|
AlarmaMusical(
|
||||||
|
id: 'dismiss-test',
|
||||||
|
nombre: 'Despertar',
|
||||||
|
hora: 7,
|
||||||
|
minuto: 30,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||||
|
diasSemana: const [],
|
||||||
|
snoozeMinutos: 5,
|
||||||
|
emisora: const Emisora(
|
||||||
|
uuid: 'e1',
|
||||||
|
nombre: 'Radio Uno',
|
||||||
|
url: 'https://radio.example/stream',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return _Env(radio: radio, android: android, estadoAlarmas: estadoAlarmas);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Env {
|
||||||
|
_Env({
|
||||||
|
required this.radio,
|
||||||
|
required this.android,
|
||||||
|
required this.estadoAlarmas,
|
||||||
|
});
|
||||||
|
final EstadoRadio radio;
|
||||||
|
final FakePuertoAlarmasAndroid android;
|
||||||
|
final EstadoAlarmas estadoAlarmas;
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
estadoAlarmas.dispose();
|
||||||
|
android.dispose();
|
||||||
|
radio.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final l10n = lookupAppLocalizations(const Locale('es'));
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('PantallaAlarmaSonando dismiss guard (Phase 5)', () {
|
||||||
|
testWidgets(
|
||||||
|
'posponer: cuando canPop es true, Navigator.pop es llamado y SystemNavigator.pop NO (S5-R1-A)',
|
||||||
|
(tester) async {
|
||||||
|
final env = await _buildEnv();
|
||||||
|
addTearDown(env.dispose);
|
||||||
|
final spy = _SystemNavigatorSpy()..install();
|
||||||
|
addTearDown(spy.uninstall);
|
||||||
|
|
||||||
|
await _montarConHistorial(
|
||||||
|
tester,
|
||||||
|
android: env.android,
|
||||||
|
estadoAlarmas: env.estadoAlarmas,
|
||||||
|
radio: env.radio,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the alarm screen is on top of a stack (canPop == true)
|
||||||
|
expect(find.byType(PantallaAlarmaSonando), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text(l10n.alarmSnoozeOptionLabel(5)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Screen should be dismissed via Navigator.pop (stack pop)
|
||||||
|
expect(find.byType(PantallaAlarmaSonando), findsNothing);
|
||||||
|
// SystemNavigator.pop must NOT have been called
|
||||||
|
expect(spy.popCalls, 0,
|
||||||
|
reason: 'SystemNavigator.pop must not be called when canPop is true');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'posponer: cuando canPop es false (root), SystemNavigator.pop es llamado (S5-R1-B)',
|
||||||
|
(tester) async {
|
||||||
|
final env = await _buildEnv();
|
||||||
|
addTearDown(env.dispose);
|
||||||
|
final spy = _SystemNavigatorSpy()..install();
|
||||||
|
addTearDown(spy.uninstall);
|
||||||
|
|
||||||
|
await _montarComoRaiz(
|
||||||
|
tester,
|
||||||
|
android: env.android,
|
||||||
|
estadoAlarmas: env.estadoAlarmas,
|
||||||
|
radio: env.radio,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(PantallaAlarmaSonando), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text(l10n.alarmSnoozeOptionLabel(5)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// SystemNavigator.pop must be called exactly once
|
||||||
|
expect(spy.popCalls, 1,
|
||||||
|
reason: 'SystemNavigator.pop must be called when canPop is false');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'detener: cuando canPop es true, Navigator.pop es llamado y SystemNavigator.pop NO (S5-R1-A)',
|
||||||
|
(tester) async {
|
||||||
|
final env = await _buildEnv();
|
||||||
|
addTearDown(env.dispose);
|
||||||
|
final spy = _SystemNavigatorSpy()..install();
|
||||||
|
addTearDown(spy.uninstall);
|
||||||
|
|
||||||
|
await _montarConHistorial(
|
||||||
|
tester,
|
||||||
|
android: env.android,
|
||||||
|
estadoAlarmas: env.estadoAlarmas,
|
||||||
|
radio: env.radio,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text(l10n.stopAlarmAction));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(PantallaAlarmaSonando), findsNothing);
|
||||||
|
expect(spy.popCalls, 0,
|
||||||
|
reason: 'SystemNavigator.pop must not be called when canPop is true');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'detener: cuando canPop es false (root), SystemNavigator.pop es llamado (S5-R1-B)',
|
||||||
|
(tester) async {
|
||||||
|
final env = await _buildEnv();
|
||||||
|
addTearDown(env.dispose);
|
||||||
|
final spy = _SystemNavigatorSpy()..install();
|
||||||
|
addTearDown(spy.uninstall);
|
||||||
|
|
||||||
|
await _montarComoRaiz(
|
||||||
|
tester,
|
||||||
|
android: env.android,
|
||||||
|
estadoAlarmas: env.estadoAlarmas,
|
||||||
|
radio: env.radio,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text(l10n.stopAlarmAction));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(spy.popCalls, 1,
|
||||||
|
reason: 'SystemNavigator.pop must be called when canPop is false');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pluriwave/modelos/alarma_musical.dart';
|
||||||
|
import 'package:pluriwave/servicios/servicio_alarmas_android.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
const channel = MethodChannel('pluriwave/alarm_scheduler');
|
||||||
|
late List<MethodCall> llamadas;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
llamadas = [];
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(channel, (call) async {
|
||||||
|
llamadas.add(call);
|
||||||
|
if (call.method == 'scheduleAlarm') return true;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(channel, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'programar includes preNoticeTemplate with {minutes} placeholder in MethodChannel call',
|
||||||
|
() async {
|
||||||
|
final servicio = ServicioAlarmasAndroid(channel: channel);
|
||||||
|
final alarma = AlarmaMusical(
|
||||||
|
id: 'test-alarm',
|
||||||
|
nombre: 'Morning alarm',
|
||||||
|
hora: 7,
|
||||||
|
minuto: 0,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||||
|
diasSemana: const [],
|
||||||
|
proximaEjecucion: DateTime(2099, 1, 1, 7, 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
await servicio.programar(alarma);
|
||||||
|
|
||||||
|
final llamada = llamadas.singleWhere((c) => c.method == 'scheduleAlarm');
|
||||||
|
final args = llamada.arguments as Map<Object?, Object?>;
|
||||||
|
expect(args.containsKey('preNoticeTemplate'), isTrue,
|
||||||
|
reason: 'preNoticeTemplate must be present in scheduleAlarm args');
|
||||||
|
final template = args['preNoticeTemplate'] as String?;
|
||||||
|
expect(template, isNotNull,
|
||||||
|
reason: 'preNoticeTemplate must not be null');
|
||||||
|
expect(template, contains('{minutes}'),
|
||||||
|
reason: 'preNoticeTemplate must contain the {minutes} placeholder');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'programar preNoticeTemplate uses default locale fallback when no l10n configured',
|
||||||
|
() async {
|
||||||
|
// ServicioAlarmasAndroid falls back to es locale when no l10n is configured
|
||||||
|
final servicio = ServicioAlarmasAndroid(channel: channel);
|
||||||
|
final alarma = AlarmaMusical(
|
||||||
|
id: 'test-alarm-2',
|
||||||
|
nombre: 'Alarm',
|
||||||
|
hora: 8,
|
||||||
|
minuto: 30,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||||
|
diasSemana: const [],
|
||||||
|
proximaEjecucion: DateTime(2099, 1, 2, 8, 30),
|
||||||
|
);
|
||||||
|
|
||||||
|
await servicio.programar(alarma);
|
||||||
|
|
||||||
|
final llamada = llamadas.singleWhere((c) => c.method == 'scheduleAlarm');
|
||||||
|
final args = llamada.arguments as Map<Object?, Object?>;
|
||||||
|
final template = args['preNoticeTemplate'] as String?;
|
||||||
|
// The template must contain the literal placeholder string
|
||||||
|
expect(template, contains('{minutes}'));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -169,4 +169,64 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Phase 1 (eq-device-autoswitch-ux): nombresDispositivos persistence
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
group('ServicioEcualizador — nombresDispositivos (eq-device-autoswitch-ux)', () {
|
||||||
|
// 1.1 RED — cargarNombresDispositivos returns empty map when SP key absent
|
||||||
|
test('1.1 cargar returns empty nombresDispositivos when SP key absent', () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final servicio = ServicioEcualizador(prefs: prefs);
|
||||||
|
final config = await servicio.cargar();
|
||||||
|
expect(config.nombresDispositivos, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1.2 RED — guardarNombresDispositivos writes to eq_nombres_dispositivos_v1 SP key
|
||||||
|
test('1.2 guardarNombresDispositivos persists names and cargar restores them', () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final servicio = ServicioEcualizador(prefs: prefs);
|
||||||
|
|
||||||
|
await servicio.guardarNombresDispositivos({'bt_a2dp:AA:BB': 'Living Room BT'});
|
||||||
|
final config = await servicio.cargar();
|
||||||
|
|
||||||
|
expect(config.nombresDispositivos['bt_a2dp:AA:BB'], equals('Living Room BT'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1.3 RED — round-trip: save → load returns same map
|
||||||
|
test('1.3 round-trip save → cargar preserves all entries', () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final servicio = ServicioEcualizador(prefs: prefs);
|
||||||
|
const nombres = {
|
||||||
|
'bt_a2dp:AA:BB': 'Living Room BT',
|
||||||
|
'wired_headset': 'Office Headset',
|
||||||
|
'builtin_speaker': 'Built-in',
|
||||||
|
};
|
||||||
|
|
||||||
|
await servicio.guardarNombresDispositivos(nombres);
|
||||||
|
final config = await servicio.cargar();
|
||||||
|
|
||||||
|
expect(config.nombresDispositivos, equals(nombres));
|
||||||
|
});
|
||||||
|
|
||||||
|
// guardarConfiguracion also persists nombresDispositivos
|
||||||
|
test('1.7 guardarConfiguracion persists nombresDispositivos round-trip', () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final servicio = ServicioEcualizador(prefs: prefs);
|
||||||
|
final config = ConfiguracionEcualizador(
|
||||||
|
principal: PresetEcualizador.flat,
|
||||||
|
porEmisora: {},
|
||||||
|
eqMultiDeviceEnabled: true,
|
||||||
|
presetsDispositivo: {},
|
||||||
|
presetsMatriz: {},
|
||||||
|
nombresDispositivos: {'bt_a2dp:AA:BB': 'My Speaker'},
|
||||||
|
);
|
||||||
|
|
||||||
|
await servicio.guardarConfiguracion(config);
|
||||||
|
final restored = await servicio.cargar();
|
||||||
|
|
||||||
|
expect(restored.nombresDispositivos['bt_a2dp:AA:BB'], equals('My Speaker'));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user