import 'dart:async'; import 'package:flutter/material.dart'; /// Live-updating countdown to a target [DateTime]. /// /// Rebuilds once per second and renders one of: /// * "in 3d 4h" — when more than a day out /// * "in 4h 12m" — when same-day /// * "in 12m 30s" — within the hour /// * "Starting now!" — within the final minute window /// * "Ended" — once the target has passed by more than the [grace] window /// /// Pass [compact] true to render only the duration text (used in cards); /// false renders a labelled card-friendly block (used on the detail screen). class CountdownTimer extends StatefulWidget { const CountdownTimer({ super.key, required this.target, this.compact = true, this.grace = const Duration(minutes: 60), }); final DateTime target; final bool compact; /// How long after [target] we still show "Starting now!" before flipping /// to "Ended". Defaults to an hour so an in-progress match stays visible. final Duration grace; @override State createState() => _CountdownTimerState(); } class _CountdownTimerState extends State { Timer? _timer; @override void initState() { super.initState(); _timer = Timer.periodic(const Duration(seconds: 1), (_) { if (mounted) setState(() {}); }); } @override void dispose() { _timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final now = DateTime.now(); final diff = widget.target.difference(now); final label = _formatLabel(diff); final isLive = diff.isNegative && diff.abs() < widget.grace; final isEnded = diff.isNegative && diff.abs() >= widget.grace; final Color bg; final Color fg; if (isEnded) { bg = scheme.surfaceContainerHighest; fg = scheme.onSurfaceVariant; } else if (isLive) { bg = scheme.tertiaryContainer; fg = scheme.onTertiaryContainer; } else { bg = scheme.primaryContainer; fg = scheme.onPrimaryContainer; } if (widget.compact) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(999), ), child: Text( label, style: theme.textTheme.labelSmall?.copyWith( color: fg, fontWeight: FontWeight.w600, ), ), ); } return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isEnded ? Icons.event_busy : isLive ? Icons.sports_soccer : Icons.timer_outlined, color: fg, ), const SizedBox(width: 12), Text( label, style: theme.textTheme.titleMedium?.copyWith( color: fg, fontWeight: FontWeight.w700, ), ), ], ), ); } String _formatLabel(Duration diff) { if (diff.isNegative) { if (diff.abs() < widget.grace) return 'Starting now!'; return 'Ended'; } if (diff.inSeconds <= 60) return 'Starting now!'; if (diff.inDays >= 1) { final days = diff.inDays; final hours = diff.inHours - days * 24; if (hours == 0) { return 'in ${days}d'; } return 'in ${days}d ${hours}h'; } if (diff.inHours >= 1) { final hours = diff.inHours; final minutes = diff.inMinutes - hours * 60; return 'in ${hours}h ${minutes}m'; } final minutes = diff.inMinutes; final seconds = diff.inSeconds - minutes * 60; return 'in ${minutes}m ${seconds}s'; } }