fix(alarma-sonando): add the schedule pill and restyle the station name

Audit 9.3 (t4 line 415): the ringing screen never showed its schedule
pill. Reuses the alarmScheduleOnce/alarmScheduleWeekdays ARB keys that
already existed with no consumer, plus a new alarmScheduleDaily key
(translated to all 13 locales) for the recurring-alarm case shown in
the prototype.

Audit 9.7 (t4 line 423): the station name was cardTitle (14.5px/w700),
5.5px and 100 weight units under the prototype's 20px/w800.

Audit 9.9 (snooze tiles as number-over-unit) is intentionally NOT
included in this commit: it would require the tile to stop rendering
as a single flat "{minutes} min" Text node, which is exactly what
pantalla_alarma_sonando_dismiss_guard_test.dart taps via
find.text(l10n.alarmSnoozeOptionLabel(N)) in four places. That file
must stay untouched, so this restyle is deferred pending a decision
on how to restructure the tap target safely.
This commit is contained in:
2026-07-29 23:33:17 +02:00
parent dcb716cbc3
commit 9840205a83
29 changed files with 215 additions and 1 deletions
+1
View File
@@ -570,6 +570,7 @@
"alarmRingingFallbackActive": "يعمل الآن باستخدام الصوت الداخلي الآمن.",
"alarmRingingPreparingFallback": "جارٍ تجهيز الصوت الداخلي الآمن.",
"alarmRingingTryingStation": "جارٍ محاولة تشغيل محطتك بأعلى جودة متاحة.",
"alarmScheduleDaily": "كل صباح",
"alarmScheduleOnce": "مرة واحدة · {date}",
"@alarmScheduleOnce": {
"placeholders": {
+1
View File
@@ -570,6 +570,7 @@
"alarmRingingFallbackActive": "অভ্যন্তরীণ নিরাপদ অডিও দিয়ে চলছে।",
"alarmRingingPreparingFallback": "অভ্যন্তরীণ নিরাপদ অডিও প্রস্তুত করা হচ্ছে।",
"alarmRingingTryingStation": "আপনার স্টেশন সর্বোচ্চ উপলভ্য মানে চালানোর চেষ্টা করা হচ্ছে।",
"alarmScheduleDaily": "প্রতিদিন সকালে",
"alarmScheduleOnce": "একবার · {date}",
"@alarmScheduleOnce": {
"placeholders": {
+1
View File
@@ -570,6 +570,7 @@
"alarmRingingFallbackActive": "Wiedergabe mit internem Sicherheitsaudio.",
"alarmRingingPreparingFallback": "Internes Sicherheitsaudio wird vorbereitet.",
"alarmRingingTryingStation": "Dein Sender wird mit der bestmöglichen verfügbaren Qualität gestartet.",
"alarmScheduleDaily": "Jeden Morgen",
"alarmScheduleOnce": "Einmal · {date}",
"@alarmScheduleOnce": {
"placeholders": {
+1
View File
@@ -676,6 +676,7 @@
"alarmRingingFallbackActive": "Playing with internal safe audio.",
"alarmRingingPreparingFallback": "Preparing internal safe audio.",
"alarmRingingTryingStation": "Trying to play your station at the highest available quality.",
"alarmScheduleDaily": "Every morning",
"alarmScheduleOnce": "Once · {date}",
"@alarmScheduleOnce": {
"placeholders": {
+1
View File
@@ -640,6 +640,7 @@
"alarmRingingFallbackActive": "Sonando con audio seguro interno.",
"alarmRingingPreparingFallback": "Preparando audio seguro interno.",
"alarmRingingTryingStation": "Intentando reproducir tu emisora con máxima calidad disponible.",
"alarmScheduleDaily": "Cada mañana",
"alarmScheduleOnce": "Una vez · {date}",
"@alarmScheduleOnce": {"placeholders": {"date": {}}},
"alarmScheduleWeekdays": "Días: {days}",
+1
View File
@@ -570,6 +570,7 @@
"alarmRingingFallbackActive": "Lecture avec laudio interne sécurisé.",
"alarmRingingPreparingFallback": "Préparation de laudio interne sécurisé.",
"alarmRingingTryingStation": "Tentative de lecture de votre station avec la meilleure qualité disponible.",
"alarmScheduleDaily": "Chaque matin",
"alarmScheduleOnce": "Une fois · {date}",
"@alarmScheduleOnce": {
"placeholders": {
+1
View File
@@ -570,6 +570,7 @@
"alarmRingingFallbackActive": "आंतरिक सुरक्षित ऑडियो के साथ चल रहा है।",
"alarmRingingPreparingFallback": "आंतरिक सुरक्षित ऑडियो तैयार किया जा रहा है।",
"alarmRingingTryingStation": "आपका स्टेशन उपलब्ध सर्वोच्च गुणवत्ता में चलाने की कोशिश की जा रही है।",
"alarmScheduleDaily": "हर सुबह",
"alarmScheduleOnce": "एक बार · {date}",
"@alarmScheduleOnce": {
"placeholders": {
+1
View File
@@ -570,6 +570,7 @@
"alarmRingingFallbackActive": "Memutar dengan audio internal yang aman.",
"alarmRingingPreparingFallback": "Menyiapkan audio internal yang aman.",
"alarmRingingTryingStation": "Mencoba memutar stasiun Anda dengan kualitas tertinggi yang tersedia.",
"alarmScheduleDaily": "Setiap pagi",
"alarmScheduleOnce": "Sekali · {date}",
"@alarmScheduleOnce": {
"placeholders": {
+1
View File
@@ -570,6 +570,7 @@
"alarmRingingFallbackActive": "Riproduzione con audio interno sicuro.",
"alarmRingingPreparingFallback": "Preparazione dellaudio interno sicuro.",
"alarmRingingTryingStation": "Tentativo di riprodurre la tua stazione con la massima qualità disponibile.",
"alarmScheduleDaily": "Ogni mattina",
"alarmScheduleOnce": "Una volta · {date}",
"@alarmScheduleOnce": {
"placeholders": {
+1
View File
@@ -570,6 +570,7 @@
"alarmRingingFallbackActive": "内部の安全な音声で再生しています。",
"alarmRingingPreparingFallback": "内部の安全な音声を準備しています。",
"alarmRingingTryingStation": "利用可能な最高音質で放送局の再生を試みています。",
"alarmScheduleDaily": "毎朝",
"alarmScheduleOnce": "1回 · {date}",
"@alarmScheduleOnce": {
"placeholders": {
+1
View File
@@ -570,6 +570,7 @@
"alarmRingingFallbackActive": "Tocando com áudio interno seguro.",
"alarmRingingPreparingFallback": "Preparando áudio interno seguro.",
"alarmRingingTryingStation": "Tentando reproduzir sua estação com a maior qualidade disponível.",
"alarmScheduleDaily": "Todas as manhãs",
"alarmScheduleOnce": "Uma vez · {date}",
"@alarmScheduleOnce": {
"placeholders": {
+1
View File
@@ -570,6 +570,7 @@
"alarmRingingFallbackActive": "Воспроизведение с внутренним безопасным аудио.",
"alarmRingingPreparingFallback": "Подготавливается внутреннее безопасное аудио.",
"alarmRingingTryingStation": "Пытаемся воспроизвести вашу станцию в максимально доступном качестве.",
"alarmScheduleDaily": "Каждое утро",
"alarmScheduleOnce": "Один раз · {date}",
"@alarmScheduleOnce": {
"placeholders": {
+1
View File
@@ -570,6 +570,7 @@
"alarmRingingFallbackActive": "正在使用内部安全音频播放。",
"alarmRingingPreparingFallback": "正在准备内部安全音频。",
"alarmRingingTryingStation": "正在尝试以最高可用音质播放你的电台。",
"alarmScheduleDaily": "每天早上",
"alarmScheduleOnce": "一次 · {date}",
"@alarmScheduleOnce": {
"placeholders": {
+6
View File
@@ -2390,6 +2390,12 @@ abstract class AppLocalizations {
/// **'Intentando reproducir tu emisora con máxima calidad disponible.'**
String get alarmRingingTryingStation;
/// No description provided for @alarmScheduleDaily.
///
/// In es, this message translates to:
/// **'Cada mañana'**
String get alarmScheduleDaily;
/// No description provided for @alarmScheduleOnce.
///
/// In es, this message translates to:
+3
View File
@@ -1311,6 +1311,9 @@ class AppLocalizationsAr extends AppLocalizations {
String get alarmRingingTryingStation =>
'جارٍ محاولة تشغيل محطتك بأعلى جودة متاحة.';
@override
String get alarmScheduleDaily => 'كل صباح';
@override
String alarmScheduleOnce(Object date) {
return 'مرة واحدة · $date';
+3
View File
@@ -1320,6 +1320,9 @@ class AppLocalizationsBn extends AppLocalizations {
String get alarmRingingTryingStation =>
'আপনার স্টেশন সর্বোচ্চ উপলভ্য মানে চালানোর চেষ্টা করা হচ্ছে।';
@override
String get alarmScheduleDaily => 'প্রতিদিন সকালে';
@override
String alarmScheduleOnce(Object date) {
return 'একবার · $date';
+3
View File
@@ -1328,6 +1328,9 @@ class AppLocalizationsDe extends AppLocalizations {
String get alarmRingingTryingStation =>
'Dein Sender wird mit der bestmöglichen verfügbaren Qualität gestartet.';
@override
String get alarmScheduleDaily => 'Jeden Morgen';
@override
String alarmScheduleOnce(Object date) {
return 'Einmal · $date';
+3
View File
@@ -1312,6 +1312,9 @@ class AppLocalizationsEn extends AppLocalizations {
String get alarmRingingTryingStation =>
'Trying to play your station at the highest available quality.';
@override
String get alarmScheduleDaily => 'Every morning';
@override
String alarmScheduleOnce(Object date) {
return 'Once · $date';
+3
View File
@@ -1322,6 +1322,9 @@ class AppLocalizationsEs extends AppLocalizations {
String get alarmRingingTryingStation =>
'Intentando reproducir tu emisora con máxima calidad disponible.';
@override
String get alarmScheduleDaily => 'Cada mañana';
@override
String alarmScheduleOnce(Object date) {
return 'Una vez · $date';
+3
View File
@@ -1332,6 +1332,9 @@ class AppLocalizationsFr extends AppLocalizations {
String get alarmRingingTryingStation =>
'Tentative de lecture de votre station avec la meilleure qualité disponible.';
@override
String get alarmScheduleDaily => 'Chaque matin';
@override
String alarmScheduleOnce(Object date) {
return 'Une fois · $date';
+3
View File
@@ -1315,6 +1315,9 @@ class AppLocalizationsHi extends AppLocalizations {
String get alarmRingingTryingStation =>
'आपका स्टेशन उपलब्ध सर्वोच्च गुणवत्ता में चलाने की कोशिश की जा रही है।';
@override
String get alarmScheduleDaily => 'हर सुबह';
@override
String alarmScheduleOnce(Object date) {
return 'एक बार · $date';
+3
View File
@@ -1323,6 +1323,9 @@ class AppLocalizationsId extends AppLocalizations {
String get alarmRingingTryingStation =>
'Mencoba memutar stasiun Anda dengan kualitas tertinggi yang tersedia.';
@override
String get alarmScheduleDaily => 'Setiap pagi';
@override
String alarmScheduleOnce(Object date) {
return 'Sekali · $date';
+3
View File
@@ -1331,6 +1331,9 @@ class AppLocalizationsIt extends AppLocalizations {
String get alarmRingingTryingStation =>
'Tentativo di riprodurre la tua stazione con la massima qualità disponibile.';
@override
String get alarmScheduleDaily => 'Ogni mattina';
@override
String alarmScheduleOnce(Object date) {
return 'Una volta · $date';
+3
View File
@@ -1272,6 +1272,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get alarmRingingTryingStation => '利用可能な最高音質で放送局の再生を試みています。';
@override
String get alarmScheduleDaily => '毎朝';
@override
String alarmScheduleOnce(Object date) {
return '1回 · $date';
+3
View File
@@ -1320,6 +1320,9 @@ class AppLocalizationsPt extends AppLocalizations {
String get alarmRingingTryingStation =>
'Tentando reproduzir sua estação com a maior qualidade disponível.';
@override
String get alarmScheduleDaily => 'Todas as manhãs';
@override
String alarmScheduleOnce(Object date) {
return 'Uma vez · $date';
+3
View File
@@ -1324,6 +1324,9 @@ class AppLocalizationsRu extends AppLocalizations {
String get alarmRingingTryingStation =>
'Пытаемся воспроизвести вашу станцию в максимально доступном качестве.';
@override
String get alarmScheduleDaily => 'Каждое утро';
@override
String alarmScheduleOnce(Object date) {
return 'Один раз · $date';
+3
View File
@@ -1267,6 +1267,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get alarmRingingTryingStation => '正在尝试以最高可用音质播放你的电台。';
@override
String get alarmScheduleDaily => '每天早上';
@override
String alarmScheduleOnce(Object date) {
return '一次 · $date';
+99 -1
View File
@@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import '../estado/estado_alarmas.dart';
import '../l10n/display_names.dart';
import '../l10n/formato_fechas.dart';
import '../l10n/gen/app_localizations.dart';
import '../modelos/alarma_musical.dart';
import '../tema/pluri_animate.dart';
@@ -204,6 +205,14 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
child: Column(
children: [
const Spacer(flex: 2),
// Audit 9.3 (t4 line 415): the schedule pill was missing
// entirely — built from `alarma.tipoProgramacion` (a field
// already on the domain model), no new plumbing.
_PildoraProgramacion(
texto: _resumenProgramacion(context, l10n, alarma),
tokens: tokens,
),
const SizedBox(height: 22),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
@@ -244,7 +253,15 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
? localizedStationName(l10n, alarma.emisora!.nombre)
: l10n.alarmRingingNotificationTitle,
textAlign: TextAlign.center,
style: type.cardTitle,
// Audit 9.7 (t4 line 423): 20px/w800/ls-.3 — this used to
// be `cardTitle` (14.5px/w700), 5.5px and 100 weight
// units under spec for the station name on a full-screen
// ringing surface.
style: type.cardTitle.copyWith(
fontSize: 20,
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
),
),
// WU11 (native-alarms delta — Ringing Screen Shows a
// Static Status Label): only rendered while this alarm was
@@ -360,6 +377,87 @@ String _hora(AlarmaMusical alarma) =>
/// call site (mirrors `_ArteEscuchar._radio` in `pantalla_inicio.dart`).
const _stopButtonRadius = 24.0;
/// Audit 9.3: the schedule pill's text, built only from
/// [AlarmaMusical.tipoProgramacion] and the fields it already carries per
/// case (`diasSemana`, `fechaUnica`) — no new domain plumbing. Reuses the
/// `alarmScheduleOnce`/`alarmScheduleWeekdays` ARB keys, which existed
/// already but had no consumer anywhere in the app.
String _resumenProgramacion(
BuildContext context,
AppLocalizations l10n,
AlarmaMusical alarma,
) {
switch (alarma.tipoProgramacion) {
case TipoProgramacionAlarma.diaria:
return l10n.alarmScheduleDaily;
case TipoProgramacionAlarma.unica:
final localeTag = Localizations.localeOf(context).toString();
return l10n.alarmScheduleOnce(
fechaCortaLocalizada(localeTag, alarma.fechaUnica ?? DateTime.now()),
);
case TipoProgramacionAlarma.diasSemana:
final dias = (List<int>.from(alarma.diasSemana)
..sort()).map((d) => _weekdayShort(l10n, d)).join(', ');
return l10n.alarmScheduleWeekdays(dias);
}
}
// Mirrors `pantalla_alarmas.dart`'s private `_weekdayShort` (kept local
// rather than shared/exported: this screen's only other tie to the alarm
// editor is the domain model itself, and duplicating a 7-line switch is
// cheaper than adding a cross-screen import for it).
String _weekdayShort(AppLocalizations l10n, int day) => switch (day) {
DateTime.monday => l10n.weekdayShortMonday,
DateTime.tuesday => l10n.weekdayShortTuesday,
DateTime.wednesday => l10n.weekdayShortWednesday,
DateTime.thursday => l10n.weekdayShortThursday,
DateTime.friday => l10n.weekdayShortFriday,
DateTime.saturday => l10n.weekdayShortSaturday,
DateTime.sunday => l10n.weekdayShortSunday,
_ => '?',
};
/// Schedule pill (audit 9.3, t4 line 415): `alarm` icon + schedule summary
/// on a warmCoral-tinted pill, matching the prototype's
/// `rgba(244,184,96,.16)` fill / `rgba(244,184,96,.45)` border exactly.
class _PildoraProgramacion extends StatelessWidget {
const _PildoraProgramacion({required this.texto, required this.tokens});
final String texto;
final PluriWaveTokens tokens;
@override
Widget build(BuildContext context) {
return DecoratedBox(
key: const ValueKey('ringing-schedule-pill'),
decoration: BoxDecoration(
color: tokens.warmCoral.withValues(alpha: 0.16),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: tokens.warmCoral.withValues(alpha: 0.45)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.alarm, size: 18, color: tokens.warmCoral),
const SizedBox(width: 6),
Text(
texto,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
letterSpacing: 0.84,
color: tokens.warmCoral,
),
),
],
),
),
);
}
}
/// Full-bleed blurred backdrop (WU11, replaces the `PluriGlassSurface` card
/// container per task 11.3). This app has no per-station artwork/favicon
/// safe to render here: `Emisora.favicon` is a network URL, and rendering
@@ -11,6 +11,7 @@ import 'package:pluriwave/pantallas/pantalla_alarma_sonando.dart';
import 'package:pluriwave/servicios/servicio_alarmas.dart';
import 'package:pluriwave/servicios/servicio_alarmas_android.dart';
import 'package:pluriwave/servicios/servicio_audio.dart';
import 'package:pluriwave/tema/pluriwave_tokens.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -282,6 +283,63 @@ void main() {
);
});
group('visual fidelity (audit 9.3, proto t4 line 415): pildora de '
'programacion', () {
testWidgets('alarma diaria: pildora "Cada manana" con icono alarm y acento '
'warmCoral', (tester) async {
await _montarPantalla(tester); // tipoProgramacion: diaria (fixture)
final pildora = find.byKey(const ValueKey('ringing-schedule-pill'));
expect(pildora, findsOneWidget);
expect(
find.descendant(
of: pildora,
matching: find.text(l10n.alarmScheduleDaily),
),
findsOneWidget,
reason: 'prototype t4 line 415: "CADA MAÑANA"',
);
expect(
find.descendant(of: pildora, matching: find.byIcon(Icons.alarm)),
findsOneWidget,
);
final caja = tester.widget<DecoratedBox>(pildora);
final decoracion = caja.decoration as BoxDecoration;
const acento = PluriWaveTokens.dark;
expect(
decoracion.color,
acento.warmCoral.withValues(alpha: 0.16),
reason: 'prototype t4 line 415: rgba(244,184,96,.16)',
);
expect(
decoracion.borderRadius,
BorderRadius.circular(999),
reason: 'prototype t4 line 415: border-radius:999px (pill)',
);
expect(
(decoracion.border as Border).top.color,
acento.warmCoral.withValues(alpha: 0.45),
reason: 'prototype t4 line 415: border rgba(244,184,96,.45)',
);
});
});
group('visual fidelity (audit 9.7, proto t4 line 423): nombre de emisora '
'20/w800', () {
testWidgets(
'el nombre de la emisora es 20px/w800/ls-.3, no cardTitle (14.5/w700)',
(tester) async {
await _montarPantalla(tester);
final texto = tester.widget<Text>(find.text('Radio Uno'));
expect(texto.style?.fontSize, 20);
expect(texto.style?.fontWeight, FontWeight.w800);
expect(texto.style?.letterSpacing, -0.3);
},
);
});
group('WU11 — estado estatico de subida de volumen (resolucion 4, sin '
'contador en vivo)', () {
testWidgets(