fix(alarmas): New pill, banner layout, vacation pill, card recurrence
Audit 7.1 (t4:325): the "New" action is a solid brand-teal pill with a plain add glyph -- was a tonal button with auto_awesome. Audit 7.2 (t4:326-330): the next-alarm banner is warmCoral-tinted with the Skip chip BESIDE the text on the same row -- was an opaque default card with the skip action stacked below as an OutlinedButton. Audit 7.3 (t4:332): the vacation row gains a trailing "d-d MON" date-range pill for the active-or-next range, built from the existing EstadoAlarmas.rangoVacacionesActivo/vacacionesProximas() accessors -- new formato_fechas.dart helper, no new state. Audit 7.4 (t4:337-345): the alarm card now shows a recurrence label next to the giant time (reusing the existing oneTimeOption/ dailyOption/weekdaysOption strings) and a themed station-icon slot next to the station name. The real per-station favicon is NOT rendered here -- same network-image hazard already documented for the ringing screen's audit 9.2 (Emisora.favicon is a network URL; Image.network hangs widget tests without a mocked HttpClient). The custom switch shape (52x32/26px thumb) is also left as Switch.adaptive -- a disclosed sub-gap, not a silent drop. Item 7.6 (_AccesoDiagnostico, an Android-reliability debug row) is a pre-approved addition not in the prototype -- informational only, no action needed.
This commit is contained in:
@@ -22,3 +22,21 @@ String diaMesLocalizado(String localeTag, DateTime fecha) =>
|
||||
/// audit 9b.4 (t4:459), the small caption under the day-month.
|
||||
String nombreDiaSemanaLocalizado(String localeTag, DateTime fecha) =>
|
||||
DateFormat.EEEE(localeTag).format(fecha);
|
||||
|
||||
/// Full weekday + month + day, locale-aware (e.g. "lunes, 3 de agosto" for
|
||||
/// `es`, "Monday, August 3" for `en`) — audit 9.4 (t4:419), the ringing
|
||||
/// screen's date line between the schedule pill and the hero time.
|
||||
String fechaLargaConDiaSemana(String localeTag, DateTime fecha) =>
|
||||
DateFormat.MMMMEEEEd(localeTag).format(fecha);
|
||||
|
||||
/// Short "d–d MON" range pill, locale-aware month abbreviation, uppercased
|
||||
/// (e.g. "4–18 AGO" for `es`, "4–18 AUG" for `en`) — audit 7.3 (t4:332),
|
||||
/// the vacation-row date-range pill on the Alarmas root. Always labels the
|
||||
/// range with the END date's month: vacation ranges are short (days to a
|
||||
/// couple of weeks), so a cross-month span is the rare case, and the
|
||||
/// prototype itself only ever shows a single abbreviation.
|
||||
String rangoFechasCorto(String localeTag, DateTime inicio, DateTime fin) {
|
||||
final dia = DateFormat.d(localeTag);
|
||||
final mes = DateFormat.MMM(localeTag).format(fin).toUpperCase();
|
||||
return '${dia.format(inicio)}–${dia.format(fin)} $mes';
|
||||
}
|
||||
|
||||
@@ -45,9 +45,26 @@ class PantallaAlarmas extends StatelessWidget {
|
||||
title: l10n.alarmScreenTitle,
|
||||
onSleepTimer: () => showPluriSleepTimerSheet(context),
|
||||
actions: [
|
||||
FilledButton.tonalIcon(
|
||||
// Audit 7.1 (t4:325): a solid brand-teal pill with a plain
|
||||
// `add` glyph -- was a tonal button with `auto_awesome`.
|
||||
FilledButton.icon(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: PluriWaveTokens.brand,
|
||||
foregroundColor: const Color(0xFF062126),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 9,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
onPressed: () => _abrirEditor(context),
|
||||
icon: const Icon(Icons.auto_awesome_rounded, size: 18),
|
||||
icon: const Icon(Icons.add_rounded, size: 18),
|
||||
label: Text(l10n.createAlarmAction),
|
||||
),
|
||||
],
|
||||
@@ -105,6 +122,7 @@ class _PanelProximaAlarma extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final tokens = context.pluriTokens;
|
||||
final proxima = estado.proximaAlarma;
|
||||
final activasSinProxima =
|
||||
estado.alarmas
|
||||
@@ -112,56 +130,87 @@ class _PanelProximaAlarma extends StatelessWidget {
|
||||
.length;
|
||||
final proximaProgramable = proxima?.proximaProgramable;
|
||||
|
||||
return PluriGlassSurface(
|
||||
glowColor: context.pluriTokens.warmCoral.withValues(alpha: 0.28),
|
||||
child: Row(
|
||||
children: [
|
||||
_AssetIcon(
|
||||
'assets/icons/alarmas/alarm_music.png',
|
||||
size: 72,
|
||||
semanticLabel: l10n.alarmIconLabel,
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
proxima == null
|
||||
? activasSinProxima > 0
|
||||
? l10n.activeAlarmsWithoutNextTitle
|
||||
: l10n.noActiveAlarms
|
||||
: l10n.nextAlarmTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
proxima == null
|
||||
? activasSinProxima > 0
|
||||
? l10n.activeAlarmsWithoutNextSubtitle(
|
||||
activasSinProxima,
|
||||
)
|
||||
: 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),
|
||||
// Audit 7.2 (t4:326-330): warmCoral-tinted card, `alarm_on` icon at
|
||||
// 26px, and the "Saltar" chip BESIDE the text on the same row -- was
|
||||
// an opaque default card with a 72px PNG and the skip action stacked
|
||||
// BELOW the text as an OutlinedButton.
|
||||
return DecoratedBox(
|
||||
key: const ValueKey('next-alarm-banner'),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.warmCoral.withValues(alpha: 0.13),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: tokens.warmCoral.withValues(alpha: 0.34)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.alarm_on, size: 26, color: tokens.warmCoral),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
proxima == null
|
||||
? activasSinProxima > 0
|
||||
? l10n.activeAlarmsWithoutNextTitle
|
||||
: l10n.noActiveAlarms
|
||||
: l10n.nextAlarmTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
proxima == null
|
||||
? activasSinProxima > 0
|
||||
? l10n.activeAlarmsWithoutNextSubtitle(
|
||||
activasSinProxima,
|
||||
)
|
||||
: l10n.createAlarmHint
|
||||
: '${_nombreVisibleAlarma(l10n, proxima)} · ${_fechaHora(l10n, proximaProgramable!)}',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (proxima != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
_ChipSaltar(onTap: () => _saltarDesdeHero(context, proxima)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Audit 7.2 (t4:329): `padding:8px 12px;radius:10;rgba(255,255,255,.08)` --
|
||||
/// plain text, no icon, unlike the previous `OutlinedButton.icon`.
|
||||
class _ChipSaltar extends StatelessWidget {
|
||||
const _ChipSaltar({required this.onTap});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return Material(
|
||||
color: Colors.white.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: InkWell(
|
||||
key: const ValueKey('hero-skip-next'),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Text(
|
||||
l10n.alarmHeroSkipAction,
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w800),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -243,17 +292,83 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_hora(alarma),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.displaySmall?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -1.5,
|
||||
),
|
||||
// Audit 7.4 (t4:339): the recurrence label sits on
|
||||
// the SAME baseline as the giant time -- it used to
|
||||
// be missing entirely.
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
_hora(alarma),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.displaySmall?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_recurrenciaCorta(l10n, alarma),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
// Audit 7.4 (t4:341): a small station-art slot
|
||||
// inline with the name -- was the name alone.
|
||||
// `Emisora.favicon` is a network URL (the same
|
||||
// hazard documented for the ringing screen's audit
|
||||
// 9.2 -- `Image.network` here would hang widget
|
||||
// tests without a mocked HttpClient), so this is a
|
||||
// themed fallback icon, not the real per-station
|
||||
// artwork, mirroring the recordings-row precedent
|
||||
// (`pantalla_grabaciones.dart`'s `_FilaGrabacion`).
|
||||
Row(
|
||||
children: [
|
||||
if (alarma.emisora != null) ...[
|
||||
DecoratedBox(
|
||||
key: const ValueKey('tarjeta-alarma-arte'),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.listSurface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: Icon(
|
||||
Icons.radio_rounded,
|
||||
size: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withValues(alpha: 0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 7),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(
|
||||
estacion,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(estacion, overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1089,6 +1204,12 @@ class _PanelVacaciones extends StatelessWidget {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final tokens = context.pluriTokens;
|
||||
final resumen = _resumenVacaciones(l10n, estado.vacaciones);
|
||||
// Audit 7.3 (t4:332): a trailing date-range pill ("4-18 AGO") for the
|
||||
// active-or-next range -- never rendered anywhere before.
|
||||
final proximas = estado.vacacionesProximas();
|
||||
final rangoRelevante =
|
||||
estado.rangoVacacionesActivo() ??
|
||||
(proximas.isEmpty ? null : proximas.first);
|
||||
return PluriGlassSurface(
|
||||
glowColor: PluriWaveTokens.skyBlue.withValues(alpha: 0.22),
|
||||
padding: EdgeInsets.zero,
|
||||
@@ -1122,6 +1243,10 @@ class _PanelVacaciones extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (rangoRelevante != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
_PildoraFechasVacaciones(rango: rangoRelevante),
|
||||
],
|
||||
const Icon(Icons.chevron_right_rounded),
|
||||
],
|
||||
),
|
||||
@@ -1168,6 +1293,40 @@ class _PanelVacaciones extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Audit 7.3 (t4:332): "4-18 AGO" -- `radius:999`, `liveGreen@.16` fill,
|
||||
/// `liveGreen@.38` border, 10.5px/w800 in `liveGreen` (matches the
|
||||
/// prototype's own `rgba(126,228,194,...)` teal, not the alarm banner's
|
||||
/// warmCoral).
|
||||
class _PildoraFechasVacaciones extends StatelessWidget {
|
||||
const _PildoraFechasVacaciones({required this.rango});
|
||||
|
||||
final RangoVacaciones rango;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = context.pluriTokens;
|
||||
final locale = AppLocalizations.of(context).localeName;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.liveGreen.withValues(alpha: 0.16),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: tokens.liveGreen.withValues(alpha: 0.38)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
|
||||
child: Text(
|
||||
rangoFechasCorto(locale, rango.inicioDia, rango.finDia),
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: tokens.liveGreen,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Audit 8.4 (t4 lines 383-390): one circular weekday button in the alarm
|
||||
/// editor's REPETIR row — was a `FilterChip`. `aspect-ratio:1` in the
|
||||
/// prototype is achieved here by the caller wrapping each instance in an
|
||||
@@ -1375,3 +1534,17 @@ String _weekdayShort(AppLocalizations l10n, int day) => switch (day) {
|
||||
// S5-R4: short dates follow the active locale (en-US = M/D/Y, ja = Y/M/D).
|
||||
String _fechaCorta(AppLocalizations l10n, DateTime fecha) =>
|
||||
fechaCortaLocalizada(l10n.localeName, fecha);
|
||||
|
||||
/// Audit 7.4 (t4:339): a compact recurrence label next to the alarm card's
|
||||
/// giant time. Reuses the SAME generic labels the editor's own
|
||||
/// `TipoProgramacionAlarma` `SegmentedButton` already shows (`oneTimeOption`
|
||||
/// / `dailyOption` / `weekdaysOption`) rather than inventing a new, more
|
||||
/// specific ARB string -- honest given the space (12px, next to a 34px
|
||||
/// time) genuinely only fits a short word, not a full weekday list.
|
||||
String _recurrenciaCorta(AppLocalizations l10n, AlarmaMusical alarma) {
|
||||
return switch (alarma.tipoProgramacion) {
|
||||
TipoProgramacionAlarma.diaria => l10n.dailyOption,
|
||||
TipoProgramacionAlarma.diasSemana => l10n.weekdaysOption,
|
||||
TipoProgramacionAlarma.unica => l10n.oneTimeOption,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user