import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../l10n/app_localizations.dart'; import 'radar_timeline.dart'; /// Renders [age] at a scale a reader can take in at a glance. /// /// Minutes alone stop being informative quickly: data three days old reads as /// "Aggiornato 4320 minuti fa", which is a number nobody converts in their /// head. The unit steps up so the magnitude is obvious even when the exact /// figure is not. String formatDataAge(AppLocalizations l10n, Duration age) { final clamped = age.isNegative ? Duration.zero : age; if (clamped.inHours < 1) return l10n.dataAgeMinutes(clamped.inMinutes); if (clamped.inDays < 1) return l10n.dataAgeHours(clamped.inHours); return l10n.dataAgeDays(clamped.inDays); } /// States the age of the radar data, and says so loudly when it is stale. /// /// Radar shown without its age is radar the reader will assume is current. That /// is the failure mode that matters for a weather app: someone deciding whether /// to leave the house based on a picture of the sky from an hour ago. So the /// age is always on screen, not only when something has gone wrong. class DataAgeBanner extends ConsumerWidget { const DataAgeBanner({super.key, this.now}); /// Injectable clock for tests. final DateTime? now; /// Beyond this the data stops being "the current picture". /// /// Frames arrive every five minutes, so a gap this size means several /// publishes were missed, not that one is slightly late. static const Duration staleAfter = Duration(minutes: 20); @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final state = ref.watch(radarTimelineProvider); final manifest = state.manifest; if (manifest == null || manifest.isEmpty) { // Nothing to date. Whether that is a failure or a first load is the // timeline bar's business, not this widget's. return const SizedBox.shrink(); } final age = manifest.ageAt(now ?? DateTime.now()); final ageText = formatDataAge(l10n, age); final isStale = age >= staleAfter || state.error != null; if (!isStale) { // Full width and left aligned, matching the stale variant below, so the // text does not jump across the screen when the state flips. return SizedBox( width: double.infinity, child: Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 4), child: Text( ageText, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.outline, ), ), ), ); } return Container( width: double.infinity, color: theme.colorScheme.errorContainer, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon( Icons.warning_amber_rounded, size: 18, color: theme.colorScheme.onErrorContainer, ), const SizedBox(width: 8), Expanded( child: Text( // The age goes inside the warning rather than replacing it: "not // updated" alone leaves the reader guessing how far off it is. state.error != null ? '${l10n.dataUnavailable} ยท $ageText' : l10n.radarStaleWithAge(ageText), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onErrorContainer, ), ), ), ], ), ); } }