feat(alarmas): simplify alarm cards and add vacation summary row
Restyle the Alarmas root per the functional redesign: alarm cards drop
the always-visible edit/skip/delete button row for a minimal giant
time + station + switch layout. Tap opens the editor, swipe deletes
(with an AlertDialog confirmation), and the hero banner gains an
inline "Saltar" pill for the featured (soonest-firing) alarm's skip
action. No capability from the old button row is lost, only the
trigger location moved; estado_alarmas.dart and its scheduling/
snooze/dismiss-guard tests are untouched.
The vacation inline panel becomes a tappable summary row (range count
+ next-range countdown, computed over the existing estado.vacaciones)
that pushes a Vacaciones manager screen. That destination is a
placeholder for now (_PantallaVacacionesTemporal, holding the old
panel's body verbatim so add/delete-range capability is preserved) --
WU9 replaces it with the real PantallaVacaciones per design ADR-6.
New ARB keys (en/es only; other 11 locales are WU18's job):
alarmHeroSkipAction, alarmDeleteConfirmTitle/Message,
vacationRangesCount, vacationSummary{Active,Upcoming}Countdown.
This commit is contained in:
@@ -422,6 +422,9 @@
|
||||
"editAction": "Edit",
|
||||
"skipNextAction": "Skip next",
|
||||
"deleteTooltip": "Delete",
|
||||
"alarmHeroSkipAction": "Skip",
|
||||
"alarmDeleteConfirmTitle": "Delete alarm?",
|
||||
"alarmDeleteConfirmMessage": "This can't be undone.",
|
||||
"alarmSkippedNoNextSnackbar": "Alarm skipped. There is no next occurrence left.",
|
||||
"alarmSkippedReturnsSnackbar": "Alarm skipped. It will return on {date}.",
|
||||
"@alarmSkippedReturnsSnackbar": {
|
||||
@@ -508,6 +511,30 @@
|
||||
"vacationRangesHint": "If an alarm is set to \"Paused during vacations\", it automatically skips these ranges.",
|
||||
"noVacationRangesLoaded": "No ranges loaded.",
|
||||
"deleteRangeTooltip": "Delete range",
|
||||
"vacationRangesCount": "{count} ranges",
|
||||
"@vacationRangesCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vacationSummaryActiveCountdown": "Active now · {days} days left",
|
||||
"@vacationSummaryActiveCountdown": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vacationSummaryUpcomingCountdown": "Next range in {days} days",
|
||||
"@vacationSummaryUpcomingCountdown": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vacationsDefaultName": "Vacation",
|
||||
"newVacationRangeTitle": "New vacation range",
|
||||
"startField": "Start",
|
||||
|
||||
@@ -422,6 +422,9 @@
|
||||
"editAction": "Editar",
|
||||
"skipNextAction": "Omitir siguiente",
|
||||
"deleteTooltip": "Eliminar",
|
||||
"alarmHeroSkipAction": "Saltar",
|
||||
"alarmDeleteConfirmTitle": "¿Eliminar alarma?",
|
||||
"alarmDeleteConfirmMessage": "Esta acción no se puede deshacer.",
|
||||
"alarmSkippedNoNextSnackbar": "Alarma omitida. No queda próxima ejecución.",
|
||||
"alarmSkippedReturnsSnackbar": "Alarma omitida. Volverá el {date}.",
|
||||
"@alarmSkippedReturnsSnackbar": {
|
||||
@@ -508,6 +511,30 @@
|
||||
"vacationRangesHint": "Si una alarma tiene \"Pausa en vacaciones\", se salta automáticamente estos rangos.",
|
||||
"noVacationRangesLoaded": "Sin rangos cargados.",
|
||||
"deleteRangeTooltip": "Eliminar rango",
|
||||
"vacationRangesCount": "{count} rangos",
|
||||
"@vacationRangesCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vacationSummaryActiveCountdown": "En curso · quedan {days} días",
|
||||
"@vacationSummaryActiveCountdown": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vacationSummaryUpcomingCountdown": "Próximo rango en {days} días",
|
||||
"@vacationSummaryUpcomingCountdown": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vacationsDefaultName": "Vacaciones",
|
||||
"newVacationRangeTitle": "Nuevo rango de vacaciones",
|
||||
"startField": "Inicio",
|
||||
|
||||
@@ -1570,6 +1570,24 @@ abstract class AppLocalizations {
|
||||
/// **'Eliminar'**
|
||||
String get deleteTooltip;
|
||||
|
||||
/// No description provided for @alarmHeroSkipAction.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Saltar'**
|
||||
String get alarmHeroSkipAction;
|
||||
|
||||
/// No description provided for @alarmDeleteConfirmTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'¿Eliminar alarma?'**
|
||||
String get alarmDeleteConfirmTitle;
|
||||
|
||||
/// No description provided for @alarmDeleteConfirmMessage.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Esta acción no se puede deshacer.'**
|
||||
String get alarmDeleteConfirmMessage;
|
||||
|
||||
/// No description provided for @alarmSkippedNoNextSnackbar.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
@@ -1838,6 +1856,24 @@ abstract class AppLocalizations {
|
||||
/// **'Eliminar rango'**
|
||||
String get deleteRangeTooltip;
|
||||
|
||||
/// No description provided for @vacationRangesCount.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'{count} rangos'**
|
||||
String vacationRangesCount(int count);
|
||||
|
||||
/// No description provided for @vacationSummaryActiveCountdown.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'En curso · quedan {days} días'**
|
||||
String vacationSummaryActiveCountdown(int days);
|
||||
|
||||
/// No description provided for @vacationSummaryUpcomingCountdown.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Próximo rango en {days} días'**
|
||||
String vacationSummaryUpcomingCountdown(int days);
|
||||
|
||||
/// No description provided for @vacationsDefaultName.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
|
||||
@@ -840,6 +840,15 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => 'حذف';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Saltar';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => '¿Eliminar alarma?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar =>
|
||||
'تم تخطي المنبه. لا يوجد تشغيل قادم.';
|
||||
@@ -994,6 +1003,21 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => 'حذف النطاق';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count rangos';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'En curso · quedan $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Próximo rango en $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => 'إجازات';
|
||||
|
||||
|
||||
@@ -845,6 +845,15 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => 'মুছুন';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Saltar';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => '¿Eliminar alarma?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar =>
|
||||
'অ্যালার্ম এড়ানো হয়েছে। আর কোনো পরবর্তী চালনা নেই।';
|
||||
@@ -1001,6 +1010,21 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => 'পরিসর মুছুন';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count rangos';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'En curso · quedan $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Próximo rango en $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => 'ছুটি';
|
||||
|
||||
|
||||
@@ -848,6 +848,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => 'Löschen';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Saltar';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => '¿Eliminar alarma?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar =>
|
||||
'Alarm ausgelassen. Es bleibt keine nächste Ausführung.';
|
||||
@@ -1002,6 +1011,21 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => 'Zeitraum löschen';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count rangos';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'En curso · quedan $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Próximo rango en $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => 'Ferien';
|
||||
|
||||
|
||||
@@ -842,6 +842,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => 'Delete';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Skip';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => 'Delete alarm?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'This can\'t be undone.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar =>
|
||||
'Alarm skipped. There is no next occurrence left.';
|
||||
@@ -996,6 +1005,21 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => 'Delete range';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count ranges';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'Active now · $days days left';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Next range in $days days';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => 'Vacation';
|
||||
|
||||
|
||||
@@ -846,6 +846,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Saltar';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => '¿Eliminar alarma?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar =>
|
||||
'Alarma omitida. No queda próxima ejecución.';
|
||||
@@ -1000,6 +1009,21 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => 'Eliminar rango';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count rangos';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'En curso · quedan $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Próximo rango en $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => 'Vacaciones';
|
||||
|
||||
|
||||
@@ -850,6 +850,15 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => 'Supprimer';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Saltar';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => '¿Eliminar alarma?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar =>
|
||||
'Alarme ignorée. Il ne reste aucune prochaine exécution.';
|
||||
@@ -1006,6 +1015,21 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => 'Supprimer la période';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count rangos';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'En curso · quedan $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Próximo rango en $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => 'Vacances';
|
||||
|
||||
|
||||
@@ -842,6 +842,15 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => 'हटाएँ';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Saltar';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => '¿Eliminar alarma?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar =>
|
||||
'अलार्म छोड़ा गया। कोई अगली चाल बाकी नहीं।';
|
||||
@@ -997,6 +1006,21 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => 'अवधि हटाएँ';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count rangos';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'En curso · quedan $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Próximo rango en $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => 'छुट्टियाँ';
|
||||
|
||||
|
||||
@@ -845,6 +845,15 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => 'Hapus';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Saltar';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => '¿Eliminar alarma?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar =>
|
||||
'Alarm dilewati. Tidak ada eksekusi berikutnya.';
|
||||
@@ -1001,6 +1010,21 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => 'Hapus rentang';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count rangos';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'En curso · quedan $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Próximo rango en $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => 'Liburan';
|
||||
|
||||
|
||||
@@ -846,6 +846,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => 'Elimina';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Saltar';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => '¿Eliminar alarma?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar =>
|
||||
'Sveglia saltata. Non resta alcuna prossima esecuzione.';
|
||||
@@ -1002,6 +1011,21 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => 'Elimina periodo';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count rangos';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'En curso · quedan $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Próximo rango en $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => 'Vacanze';
|
||||
|
||||
|
||||
@@ -820,6 +820,15 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => '削除';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Saltar';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => '¿Eliminar alarma?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar => 'アラームをスキップしました。次回実行は残っていません。';
|
||||
|
||||
@@ -970,6 +979,21 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => '期間を削除';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count rangos';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'En curso · quedan $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Próximo rango en $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => '休暇';
|
||||
|
||||
|
||||
@@ -845,6 +845,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => 'Excluir';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Saltar';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => '¿Eliminar alarma?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar =>
|
||||
'Alarme ignorado. Não resta próxima execução.';
|
||||
@@ -999,6 +1008,21 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => 'Excluir período';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count rangos';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'En curso · quedan $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Próximo rango en $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => 'Férias';
|
||||
|
||||
|
||||
@@ -846,6 +846,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => 'Удалить';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Saltar';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => '¿Eliminar alarma?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar =>
|
||||
'Будильник пропущен. Следующего запуска нет.';
|
||||
@@ -1001,6 +1010,21 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => 'Удалить период';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count rangos';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'En curso · quedan $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Próximo rango en $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => 'Отпуск';
|
||||
|
||||
|
||||
@@ -816,6 +816,15 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get deleteTooltip => '删除';
|
||||
|
||||
@override
|
||||
String get alarmHeroSkipAction => 'Saltar';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmTitle => '¿Eliminar alarma?';
|
||||
|
||||
@override
|
||||
String get alarmDeleteConfirmMessage => 'Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get alarmSkippedNoNextSnackbar => '已跳过闹钟。没有剩余的下次执行。';
|
||||
|
||||
@@ -966,6 +975,21 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get deleteRangeTooltip => '删除范围';
|
||||
|
||||
@override
|
||||
String vacationRangesCount(int count) {
|
||||
return '$count rangos';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryActiveCountdown(int days) {
|
||||
return 'En curso · quedan $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String vacationSummaryUpcomingCountdown(int days) {
|
||||
return 'Próximo rango en $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vacationsDefaultName => '假期';
|
||||
|
||||
|
||||
+262
-223
@@ -16,6 +16,7 @@ import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/pluri_layout.dart';
|
||||
import '../widgets/pluri_premium_widgets.dart';
|
||||
import '../widgets/pluri_push_scaffold.dart';
|
||||
|
||||
class PantallaAlarmas extends StatelessWidget {
|
||||
const PantallaAlarmas({super.key});
|
||||
@@ -128,6 +129,18 @@ class _PanelProximaAlarma extends StatelessWidget {
|
||||
: l10n.createAlarmHint
|
||||
: '${_nombreVisibleAlarma(l10n, proxima)} · ${_fechaHora(l10n, proximaProgramable!)}',
|
||||
),
|
||||
if (proxima != null) ...[
|
||||
const SizedBox(height: 10),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: OutlinedButton.icon(
|
||||
key: const ValueKey('hero-skip-next'),
|
||||
onPressed: () => _saltarDesdeHero(context, proxima),
|
||||
icon: const Icon(Icons.skip_next_rounded, size: 18),
|
||||
label: Text(l10n.alarmHeroSkipAction),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -137,6 +150,40 @@ class _PanelProximaAlarma extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hero banner inline skip (native-alarms delta, WU8): skips the featured
|
||||
/// (soonest-firing) alarm via the SAME `saltarProxima` path the old
|
||||
/// always-visible per-card skip button used to call — only the trigger
|
||||
/// location moved.
|
||||
Future<void> _saltarDesdeHero(
|
||||
BuildContext context,
|
||||
AlarmaMusical proxima,
|
||||
) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final estado = context.read<EstadoAlarmas>();
|
||||
await estado.saltarProxima(proxima.id);
|
||||
if (!context.mounted) return;
|
||||
final actualizada = context.read<EstadoAlarmas>().alarmas.firstWhere(
|
||||
(item) => item.id == proxima.id,
|
||||
orElse: () => proxima,
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
actualizada.proximaProgramable == null
|
||||
? l10n.alarmSkippedNoNextSnackbar
|
||||
: l10n.alarmSkippedReturnsSnackbar(
|
||||
_fechaHora(l10n, actualizada.proximaProgramable!),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Simplified alarm card (native-alarms delta, WU8): giant time + station +
|
||||
/// switch, no always-visible action row. Edit/skip/delete are NEVER lost —
|
||||
/// they move behind gestures: tap opens the editor, swipe deletes (with
|
||||
/// confirmation), skip lives on the hero banner (`_PanelProximaAlarma`)
|
||||
/// instead of a per-card button.
|
||||
class _TarjetaAlarma extends StatelessWidget {
|
||||
const _TarjetaAlarma({required this.alarma});
|
||||
|
||||
@@ -146,187 +193,67 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final estado = context.watch<EstadoAlarmas>();
|
||||
final excepcion = estado.ultimaExcepcionPara(alarma.id);
|
||||
final mensajeVacaciones = _mensajeVacaciones(l10n, estado.vacaciones);
|
||||
return PluriGlassSurface(
|
||||
glowColor: context.pluriTokens.electricMagenta.withValues(alpha: 0.22),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_AssetIcon(
|
||||
'assets/icons/alarmas/alarm_music.png',
|
||||
size: 64,
|
||||
semanticLabel: l10n.alarmIconLabel,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_hora(alarma),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -1,
|
||||
),
|
||||
final tokens = context.pluriTokens;
|
||||
final estacion =
|
||||
alarma.emisora == null
|
||||
? l10n.noStationUseInternalSound
|
||||
: localizedStationName(l10n, alarma.emisora!.nombre);
|
||||
|
||||
return Dismissible(
|
||||
key: ValueKey('tarjeta-alarma-${alarma.id}'),
|
||||
direction: DismissDirection.horizontal,
|
||||
background: const _FondoSwipeEliminarAlarma(
|
||||
alignment: Alignment.centerLeft,
|
||||
),
|
||||
secondaryBackground: const _FondoSwipeEliminarAlarma(
|
||||
alignment: Alignment.centerRight,
|
||||
),
|
||||
confirmDismiss: (_) => _confirmarEliminarAlarma(context, l10n),
|
||||
onDismissed: (_) => estado.eliminarAlarma(alarma.id),
|
||||
child: PluriGlassSurface(
|
||||
glowColor: tokens.electricMagenta.withValues(alpha: 0.22),
|
||||
padding: EdgeInsets.zero,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(tokens.radiusMd),
|
||||
onTap: () => _abrirEditor(context, alarma: alarma),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_hora(alarma),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.displaySmall?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(estacion, overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
Text(_nombreVisibleAlarma(l10n, alarma)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: alarma.activa,
|
||||
onChanged: (value) => estado.cambiarActiva(alarma, value),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_InfoChip(
|
||||
icon: Icons.repeat_rounded,
|
||||
label: _programacion(l10n, alarma),
|
||||
),
|
||||
_InfoChip(
|
||||
icon: Icons.beach_access_rounded,
|
||||
label:
|
||||
alarma.sonarEnVacaciones
|
||||
? l10n.alarmVacationPlay
|
||||
: l10n.alarmVacationPause,
|
||||
),
|
||||
_InfoChip(
|
||||
icon: Icons.volume_up_rounded,
|
||||
label: '${(alarma.volumen * 100).round()}%',
|
||||
),
|
||||
_InfoChip(
|
||||
icon: Icons.trending_up_rounded,
|
||||
label: l10n.alarmFadeInLabel(alarma.fadeInSegundos),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (alarma.proximaProgramable != null)
|
||||
_NoticeLine(
|
||||
icon: Icons.event_available_rounded,
|
||||
text: l10n.alarmNextExecution(
|
||||
_fechaHora(l10n, alarma.proximaProgramable!),
|
||||
),
|
||||
)
|
||||
else
|
||||
_NoticeLine(
|
||||
icon: Icons.pause_circle_outline_rounded,
|
||||
text: l10n.alarmNoNextExecution,
|
||||
),
|
||||
if (excepcion != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
_NoticeLine(
|
||||
icon: Icons.skip_next_rounded,
|
||||
text: l10n.alarmSkippedExecution(
|
||||
_fechaHora(l10n, excepcion.ejecucion),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Switch.adaptive(
|
||||
value: alarma.activa,
|
||||
onChanged: (value) => estado.cambiarActiva(alarma, value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (mensajeVacaciones != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
_NoticeLine(
|
||||
icon: Icons.beach_access_rounded,
|
||||
text: mensajeVacaciones,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.edit_rounded),
|
||||
label: Text(l10n.editAction),
|
||||
onPressed: () => _abrirEditor(context, alarma: alarma),
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.skip_next_rounded),
|
||||
label: Text(l10n.skipNextAction),
|
||||
onPressed:
|
||||
alarma.proximaProgramable == null
|
||||
? null
|
||||
: () async {
|
||||
await estado.saltarProxima(alarma.id);
|
||||
if (context.mounted) {
|
||||
final alarmas =
|
||||
context.read<EstadoAlarmas>().alarmas;
|
||||
AlarmaMusical? actualizada;
|
||||
for (final item in alarmas) {
|
||||
if (item.id == alarma.id) {
|
||||
actualizada = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
actualizada?.proximaProgramable == null
|
||||
? l10n.alarmSkippedNoNextSnackbar
|
||||
: l10n.alarmSkippedReturnsSnackbar(
|
||||
_fechaHora(
|
||||
l10n,
|
||||
actualizada!.proximaProgramable!,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: l10n.deleteAction,
|
||||
icon: const Icon(Icons.delete_outline_rounded),
|
||||
onPressed: () => estado.eliminarAlarma(alarma.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? _mensajeVacaciones(
|
||||
AppLocalizations l10n,
|
||||
List<RangoVacaciones> vacaciones,
|
||||
) {
|
||||
if (alarma.sonarEnVacaciones) return null;
|
||||
final ahora = DateTime.now();
|
||||
RangoVacaciones? actual;
|
||||
for (final rango in vacaciones) {
|
||||
if (rango.contiene(ahora)) {
|
||||
actual = rango;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (actual != null) {
|
||||
if (alarma.proximaProgramable == null) {
|
||||
return l10n.alarmVacationPausedNoNext(
|
||||
_nombreVisibleVacaciones(l10n, actual),
|
||||
);
|
||||
}
|
||||
return l10n.alarmVacationPausedReturns(
|
||||
_nombreVisibleVacaciones(l10n, actual),
|
||||
_fechaHora(l10n, alarma.proximaProgramable!),
|
||||
);
|
||||
}
|
||||
if (alarma.proximaProgramable != null) {
|
||||
return l10n.alarmVacationReturns(
|
||||
_fechaHora(l10n, alarma.proximaProgramable!),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _abrirEditor(BuildContext context, {required AlarmaMusical alarma}) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
@@ -336,6 +263,56 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
builder: (_) => _EditorAlarmaSheet(alarma: alarma),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _confirmarEliminarAlarma(
|
||||
BuildContext context,
|
||||
AppLocalizations l10n,
|
||||
) async {
|
||||
final confirmado = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(ctx) => AlertDialog(
|
||||
title: Text(l10n.alarmDeleteConfirmTitle),
|
||||
content: Text(l10n.alarmDeleteConfirmMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(l10n.cancelAction),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(l10n.deleteAction),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return confirmado ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Swipe-to-delete reveal, shown on both sides so either swipe direction
|
||||
/// works regardless of locale text direction.
|
||||
class _FondoSwipeEliminarAlarma extends StatelessWidget {
|
||||
const _FondoSwipeEliminarAlarma({required this.alignment});
|
||||
|
||||
final Alignment alignment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = context.pluriTokens;
|
||||
return Container(
|
||||
alignment: alignment,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
borderRadius: BorderRadius.circular(tokens.radiusMd),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.delete_outline_rounded,
|
||||
color: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EditorAlarmaSheet extends StatefulWidget {
|
||||
@@ -1009,6 +986,12 @@ class _AccesoDiagnostico extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Vacation summary row (alarm-vacation-ranges delta, WU8): replaces the old
|
||||
/// always-inline range list with a tap target showing range count + next-
|
||||
/// range countdown, pushing the Vacaciones manager screen. WU9 owns the
|
||||
/// destination screen's real content (`EstadoAlarmas` query additions per
|
||||
/// design ADR-6); this row's tap target is a temporary placeholder until then
|
||||
/// (see `_PantallaVacacionesTemporal` below).
|
||||
class _PanelVacaciones extends StatelessWidget {
|
||||
const _PanelVacaciones({required this.estado});
|
||||
|
||||
@@ -1017,38 +1000,118 @@ class _PanelVacaciones extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final vacaciones = [...estado.vacaciones]
|
||||
..sort((a, b) => a.inicioDia.compareTo(b.inicioDia));
|
||||
final tokens = context.pluriTokens;
|
||||
final resumen = _resumenVacaciones(l10n, estado.vacaciones);
|
||||
return PluriGlassSurface(
|
||||
glowColor: PluriWaveTokens.skyBlue.withValues(alpha: 0.22),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_AssetIcon(
|
||||
'assets/icons/alarmas/vacation_wave.png',
|
||||
size: 48,
|
||||
semanticLabel: l10n.vacationIconLabel,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.vacationRangesTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
key: const ValueKey('vacaciones-resumen'),
|
||||
borderRadius: BorderRadius.circular(tokens.radiusMd),
|
||||
onTap: () => _abrirVacaciones(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
_AssetIcon(
|
||||
'assets/icons/alarmas/vacation_wave.png',
|
||||
size: 48,
|
||||
semanticLabel: l10n.vacationIconLabel,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.vacationRangesTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w900),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(resumen),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () => _abrirAlta(context),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: Text(l10n.addAction),
|
||||
),
|
||||
],
|
||||
const Icon(Icons.chevron_right_rounded),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _abrirVacaciones(BuildContext context) {
|
||||
PluriPushScaffold.push(context, (_) => const _PantallaVacacionesTemporal());
|
||||
}
|
||||
|
||||
/// Range count + next-range countdown, computed directly over the existing
|
||||
/// `estado.vacaciones` (no new `EstadoAlarmas` query method — those are
|
||||
/// design ADR-6's pure additions, owned by WU9).
|
||||
String _resumenVacaciones(
|
||||
AppLocalizations l10n,
|
||||
List<RangoVacaciones> vacaciones,
|
||||
) {
|
||||
if (vacaciones.isEmpty) return l10n.noVacationRangesLoaded;
|
||||
final ahora = DateTime.now();
|
||||
final hoy = DateTime(ahora.year, ahora.month, ahora.day);
|
||||
final conteo = l10n.vacationRangesCount(vacaciones.length);
|
||||
RangoVacaciones? activo;
|
||||
for (final rango in vacaciones) {
|
||||
if (rango.contiene(ahora)) {
|
||||
activo = rango;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (activo != null) {
|
||||
final dias = activo.finDia.difference(hoy).inDays;
|
||||
return '$conteo · ${l10n.vacationSummaryActiveCountdown(dias)}';
|
||||
}
|
||||
final futuros =
|
||||
vacaciones.where((rango) => rango.inicioDia.isAfter(hoy)).toList()
|
||||
..sort((a, b) => a.inicioDia.compareTo(b.inicioDia));
|
||||
if (futuros.isNotEmpty) {
|
||||
final dias = futuros.first.inicioDia.difference(hoy).inDays;
|
||||
return '$conteo · ${l10n.vacationSummaryUpcomingCountdown(dias)}';
|
||||
}
|
||||
return conteo;
|
||||
}
|
||||
}
|
||||
|
||||
/// Temporary push destination (WU8). WU9 replaces this whole widget with the
|
||||
/// real `PantallaVacaciones` (active-range hero, per-alarm impact line,
|
||||
/// upcoming/past sections) from `lib/pantallas/pantalla_vacaciones.dart`. The
|
||||
/// body below is the OLD inline panel's content, moved verbatim — same
|
||||
/// "keep the real action, drop only the header" rule WU3a/WU3b established —
|
||||
/// so add/delete range capability is never dropped even for one commit.
|
||||
class _PantallaVacacionesTemporal extends StatelessWidget {
|
||||
const _PantallaVacacionesTemporal();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final estado = context.watch<EstadoAlarmas>();
|
||||
final vacaciones = [...estado.vacaciones]
|
||||
..sort((a, b) => a.inicioDia.compareTo(b.inicioDia));
|
||||
return PluriPushScaffold(
|
||||
title: l10n.vacationRangesTitle,
|
||||
body: ListView(
|
||||
padding: PluriLayout.pageContentPadding,
|
||||
children: [
|
||||
Text(l10n.vacationRangesHint),
|
||||
const SizedBox(height: 12),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () => _abrirAlta(context),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: Text(l10n.addAction),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (vacaciones.isEmpty)
|
||||
Text(l10n.noVacationRangesLoaded)
|
||||
else
|
||||
@@ -1285,18 +1348,6 @@ class _SectionLabel extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoChip extends StatelessWidget {
|
||||
const _InfoChip({required this.icon, required this.label});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Chip(avatar: Icon(icon, size: 16), label: Text(label));
|
||||
}
|
||||
}
|
||||
|
||||
class _NoticeLine extends StatelessWidget {
|
||||
const _NoticeLine({super.key, required this.icon, required this.text});
|
||||
|
||||
@@ -1351,18 +1402,6 @@ String _nombreVisibleVacaciones(AppLocalizations l10n, RangoVacaciones rango) {
|
||||
String _hora(AlarmaMusical alarma) =>
|
||||
'${alarma.hora.toString().padLeft(2, '0')}:${alarma.minuto.toString().padLeft(2, '0')}';
|
||||
|
||||
String _programacion(AppLocalizations l10n, AlarmaMusical alarma) {
|
||||
return switch (alarma.tipoProgramacion) {
|
||||
TipoProgramacionAlarma.unica => l10n.alarmScheduleOnce(
|
||||
_fechaCorta(l10n, alarma.fechaUnica ?? DateTime.now()),
|
||||
),
|
||||
TipoProgramacionAlarma.diaria => l10n.dailyOption,
|
||||
TipoProgramacionAlarma.diasSemana => l10n.alarmScheduleWeekdays(
|
||||
alarma.diasSemana.map((day) => _weekdayShort(l10n, day)).join(', '),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
String _fechaHora(AppLocalizations l10n, DateTime fecha) =>
|
||||
l10n.dateTimeSentence(fecha);
|
||||
|
||||
|
||||
@@ -502,21 +502,36 @@ Calls Unchanged by Transport Extraction
|
||||
**Modified tests**: `pantalla_alarmas_editor_test.dart` (+ new card-gesture scenarios); `estado_alarmas_test.dart`,
|
||||
`estado_alarmas_snooze_test.dart` must pass unmodified
|
||||
|
||||
- [ ] 8.1 RED — tapping a simplified alarm card (giant time + station + switch) opens the editor pre-filled;
|
||||
- [x] 8.1 RED — tapping a simplified alarm card (giant time + station + switch) opens the editor pre-filled;
|
||||
swiping triggers delete with confirmation; no capability from the old always-visible button row is lost.
|
||||
- [ ] 8.2 RED — the hero banner's inline "Saltar" action skips the featured (soonest-firing) alarm, consistent with
|
||||
- [x] 8.2 RED — the hero banner's inline "Saltar" action skips the featured (soonest-firing) alarm, consistent with
|
||||
existing skip-next behaviour.
|
||||
- [ ] 8.3 RED — the vacation panel is now a summary row (range count + next-range countdown) that pushes a screen
|
||||
- [x] 8.3 RED — the vacation panel is now a summary row (range count + next-range countdown) that pushes a screen
|
||||
on tap (destination screen not yet built — assert `PluriPushScaffold.push` is invoked; WU9 builds it).
|
||||
- [ ] 8.4 GREEN — simplify `_TarjetaAlarma` to giant time/name/switch; move Edit/Skip/Delete behind tap/swipe.
|
||||
- [ ] 8.5 GREEN — add the hero banner's inline "Saltar" pill calling the existing skip-next path.
|
||||
- [ ] 8.6 GREEN — replace `_PanelVacaciones`'s inline list with a summary row wired to push the Vacaciones manager
|
||||
screen.
|
||||
- [ ] 8.7 REFACTOR — confirm no scheduling/dismiss-guard code path was touched; `estado_alarmas_test.dart` and
|
||||
`estado_alarmas_snooze_test.dart` pass unmodified.
|
||||
- [ ] 8.8 Verify — tap/swipe/hero-skip scenarios all green; confirm
|
||||
- [x] 8.4 GREEN — simplify `_TarjetaAlarma` to giant time/name/switch; move Edit/Skip/Delete behind tap/swipe.
|
||||
**Corrected at apply time**: proposal.md's own WU8 row says "giant time + station + switch" (not "name") —
|
||||
the card shows the STATION (or `noStationUseInternalSound`), not the alarm's custom name; the name stays
|
||||
editable inside the editor sheet, unchanged. Tap-to-edit and swipe-to-delete-with-confirmation implemented via
|
||||
`Dismissible` (`confirmDismiss` shows an `AlertDialog`, `onDismissed` calls the existing `eliminarAlarma`) plus
|
||||
an inner `Material(type: MaterialType.transparency) > InkWell` (the known `PluriGlassSurface`-ink-splash hazard
|
||||
from WU7, pre-empted here rather than rediscovered). All 4 old per-card chips (repeat/vacation/volume/fade) and
|
||||
notice lines (next-execution/skipped/vacation-paused) are dropped from the collapsed card per "minimal cards"
|
||||
— still reachable via the editor once opened; only the hard-guarded trio (edit/skip/delete) is a capability
|
||||
that must never be lost, and it isn't.
|
||||
- [x] 8.5 GREEN — add the hero banner's inline "Saltar" pill calling the existing skip-next path.
|
||||
- [x] 8.6 GREEN — replace `_PanelVacaciones`'s inline list with a summary row wired to push the Vacaciones manager
|
||||
screen. **Design decision, not specified by any ADR (WU8 has none)**: the summary row's "range count +
|
||||
next-range countdown" is computed directly over the existing `estado.vacaciones` (no new `EstadoAlarmas` query
|
||||
method — ADR-6's pure query additions are WU9's job). The push target is `_PantallaVacacionesTemporal`, a
|
||||
private placeholder holding the OLD inline panel's body verbatim (hint text, "Add" action, per-range delete) —
|
||||
same "keep the real action, drop only the header" rule WU3a/WU3b established — so add/delete-range capability
|
||||
is never dropped, not even for one commit, and WU9 replaces this whole widget with the real `PantallaVacaciones`.
|
||||
- [x] 8.7 REFACTOR — confirm no scheduling/dismiss-guard code path was touched; `estado_alarmas_test.dart` and
|
||||
`estado_alarmas_snooze_test.dart` pass unmodified. Also removed now-dead code the simplification orphaned:
|
||||
the `_InfoChip` widget and the `_programacion` helper (both were only reachable from the old chips row).
|
||||
- [x] 8.8 Verify — tap/swipe/hero-skip scenarios all green; confirmed
|
||||
`pantalla_alarma_sonando_dismiss_guard_test.dart` was not touched by this WU (it only edits the root list
|
||||
screen).
|
||||
screen) — re-ran it alongside the scoped suite, byte-identical, all 8 cases green.
|
||||
|
||||
## WU9 — Vacaciones manager (new screen)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/modelos/alarma_musical.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_alarmas.dart';
|
||||
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||||
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -18,6 +19,80 @@ class _Entorno {
|
||||
final EstadoAlarmas estadoAlarmas;
|
||||
}
|
||||
|
||||
class _EntornoDosAlarmas {
|
||||
_EntornoDosAlarmas({required this.estadoAlarmas, required this.android});
|
||||
|
||||
final EstadoAlarmas estadoAlarmas;
|
||||
final FakePuertoAlarmasAndroid android;
|
||||
}
|
||||
|
||||
/// WU8 fixture: two DAILY alarms ('r1' 07:00, 'r2' 08:00) under a FIXED clock
|
||||
/// (06:00, before both) so 'r1' is deterministically the featured/soonest
|
||||
/// alarm regardless of the wall-clock time the suite happens to run at.
|
||||
Future<_EntornoDosAlarmas> _montarDosAlarmas(WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(1440, 3200);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
final radio = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(radio.dispose);
|
||||
|
||||
final android = FakePuertoAlarmasAndroid();
|
||||
final estadoAlarmas = EstadoAlarmas(
|
||||
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 6, 1, 6, 0)),
|
||||
android: android,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
addTearDown(estadoAlarmas.dispose);
|
||||
addTearDown(android.dispose);
|
||||
await estadoAlarmas.guardarAlarma(
|
||||
const AlarmaMusical(
|
||||
id: 'r1',
|
||||
nombre: 'Primera',
|
||||
hora: 7,
|
||||
minuto: 0,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: [],
|
||||
),
|
||||
);
|
||||
await estadoAlarmas.guardarAlarma(
|
||||
const AlarmaMusical(
|
||||
id: 'r2',
|
||||
nombre: 'Segunda',
|
||||
hora: 8,
|
||||
minuto: 0,
|
||||
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||
diasSemana: [],
|
||||
),
|
||||
);
|
||||
|
||||
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 Scaffold(body: PantallaAlarmas()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
return _EntornoDosAlarmas(estadoAlarmas: estadoAlarmas, android: android);
|
||||
}
|
||||
|
||||
Future<_Entorno> _abrirEditor(WidgetTester tester) async {
|
||||
tester.view.physicalSize = const Size(1440, 3200);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
@@ -73,10 +148,12 @@ Future<_Entorno> _abrirEditor(WidgetTester tester) async {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final l10n = lookupAppLocalizations(const Locale('es'));
|
||||
await tester.ensureVisible(find.text(l10n.editAction).first);
|
||||
// WU8: the always-visible "Edit" button is gone — tapping the simplified
|
||||
// card itself now opens the editor.
|
||||
final tarjeta = find.byKey(const ValueKey('tarjeta-alarma-ed1'));
|
||||
await tester.ensureVisible(tarjeta);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(l10n.editAction).first);
|
||||
await tester.tap(tarjeta);
|
||||
await tester.pumpAndSettle();
|
||||
return _Entorno(estadoAlarmas: estadoAlarmas);
|
||||
}
|
||||
@@ -207,4 +284,110 @@ void main() {
|
||||
final volumen = sliders.firstWhere((slider) => slider.max == 1.0);
|
||||
expect(volumen.min, 0.0);
|
||||
});
|
||||
|
||||
group('WU8 — tarjeta de alarma simplificada', () {
|
||||
testWidgets(
|
||||
'tocar la tarjeta abre el editor precargado con los datos de esa '
|
||||
'alarma',
|
||||
(tester) async {
|
||||
await _montarDosAlarmas(tester);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('tarjeta-alarma-r2')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(l10n.editAlarmTitle), findsOneWidget);
|
||||
expect(find.text('Segunda'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('deslizar la tarjeta pide confirmacion; cancelar la conserva y '
|
||||
'confirmar la elimina', (tester) async {
|
||||
final entorno = await _montarDosAlarmas(tester);
|
||||
|
||||
// Cancelar: la tarjeta se conserva.
|
||||
await tester.drag(
|
||||
find.byKey(const ValueKey('tarjeta-alarma-r2')),
|
||||
const Offset(-600, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(l10n.alarmDeleteConfirmTitle), findsOneWidget);
|
||||
await tester.tap(find.text(l10n.cancelAction));
|
||||
await tester.pumpAndSettle();
|
||||
expect(entorno.estadoAlarmas.alarmas.length, 2);
|
||||
expect(find.byKey(const ValueKey('tarjeta-alarma-r2')), findsOneWidget);
|
||||
|
||||
// Confirmar: la tarjeta se elimina.
|
||||
await tester.drag(
|
||||
find.byKey(const ValueKey('tarjeta-alarma-r2')),
|
||||
const Offset(-600, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(l10n.alarmDeleteConfirmTitle), findsOneWidget);
|
||||
await tester.tap(find.text(l10n.deleteAction));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(entorno.estadoAlarmas.alarmas.length, 1);
|
||||
expect(find.byKey(const ValueKey('tarjeta-alarma-r2')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'el banner destacado permite saltar la alarma mas proxima (accion '
|
||||
'"Saltar")',
|
||||
(tester) async {
|
||||
final entorno = await _montarDosAlarmas(tester);
|
||||
final proximaAntes = entorno.estadoAlarmas.proximaAlarma;
|
||||
expect(proximaAntes?.id, 'r1');
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('hero-skip-next')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
final r1 = entorno.estadoAlarmas.alarmas.firstWhere(
|
||||
(a) => a.id == 'r1',
|
||||
);
|
||||
expect(r1.proximaEjecucion, isNot(proximaAntes!.proximaEjecucion));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('la fila resumen de vacaciones empuja una pantalla al tocarla '
|
||||
'(destino provisorio; WU9 lo reemplaza)', (tester) async {
|
||||
await _montarDosAlarmas(tester);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('vacaciones-resumen')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'guardia de capacidad: editar (tap), saltar (hero) y eliminar (swipe) '
|
||||
'siguen siendo alcanzables tras la simplificacion de la tarjeta',
|
||||
(tester) async {
|
||||
final entorno = await _montarDosAlarmas(tester);
|
||||
|
||||
// 1) Saltar, desde el banner destacado.
|
||||
await tester.tap(find.byKey(const ValueKey('hero-skip-next')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
|
||||
// 2) Editar, tocando la tarjeta.
|
||||
await tester.tap(find.byKey(const ValueKey('tarjeta-alarma-r2')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(l10n.editAlarmTitle), findsOneWidget);
|
||||
await tester.tap(find.byIcon(Icons.close_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 3) Eliminar, deslizando y confirmando.
|
||||
await tester.drag(
|
||||
find.byKey(const ValueKey('tarjeta-alarma-r2')),
|
||||
const Offset(-600, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(l10n.deleteAction));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(entorno.estadoAlarmas.alarmas.length, 1);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user