import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/format.dart'; import '../../core/models.dart'; import '../../core/providers.dart'; import '../../core/theme.dart'; import '../../widgets/ui.dart'; const _panelWidth = 360.0; Future showCalendarPicker(BuildContext anchor) { if (!isWide(anchor)) { return showModalBottomSheet( context: anchor, isScrollControlled: true, useSafeArea: true, useRootNavigator: true, builder: (ctx) => const SingleChildScrollView( padding: EdgeInsets.fromLTRB(S.s16, S.s10, S.s16, S.s20), child: Column( mainAxisSize: MainAxisSize.min, children: [_Grabber(), _CalendarPicker()], ), ), ); } final box = anchor.findRenderObject() as RenderBox?; final rect = box != null && box.hasSize ? box.localToGlobal(Offset.zero) & box.size : null; return showGeneralDialog( context: anchor, barrierDismissible: true, barrierLabel: 'Close calendar', barrierColor: const Color(0x1A101B33), transitionDuration: const Duration(milliseconds: 160), pageBuilder: (ctx, _, _) => CustomSingleChildLayout( delegate: _AnchorLayout(rect), child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(R.dialog), boxShadow: const [ BoxShadow( color: C.shadowNavy, blurRadius: 32, offset: Offset(0, 12), ), ], ), child: Material( color: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(R.dialog), side: const BorderSide(color: C.border), ), clipBehavior: Clip.antiAlias, child: const SizedBox( width: _panelWidth, child: Padding( padding: EdgeInsets.all(S.s16), child: _CalendarPicker(), ), ), ), ), ), transitionBuilder: (ctx, a, _, child) { if (MediaQuery.disableAnimationsOf(ctx)) return child; final t = CurvedAnimation(parent: a, curve: Curves.easeOutCubic); return FadeTransition( opacity: t, child: ScaleTransition( scale: Tween(begin: 0.97, end: 1.0).animate(t), alignment: Alignment.topCenter, child: child, ), ); }, ); } class _AnchorLayout extends SingleChildLayoutDelegate { _AnchorLayout(this.anchor); final Rect? anchor; @override BoxConstraints getConstraintsForChild(BoxConstraints c) => BoxConstraints.loose(Size(c.maxWidth - 32, c.maxHeight - 32)); @override Offset getPositionForChild(Size size, Size child) { const m = S.s16; final a = anchor; if (a == null) { return Offset( (size.width - child.width) / 2, (size.height - child.height) / 2, ); } final left = (a.center.dx - child.width / 2).clamp( m, size.width - child.width - m, ); var top = a.bottom + S.s8; if (top + child.height > size.height - m) { final above = a.top - S.s8 - child.height; top = above >= m ? above : (size.height - child.height) / 2; } return Offset(left, top); } @override bool shouldRelayout(_AnchorLayout old) => old.anchor != anchor; } class _Grabber extends StatelessWidget { const _Grabber(); @override Widget build(BuildContext context) => Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: S.s12), decoration: BoxDecoration( color: C.border2, borderRadius: BorderRadius.circular(R.sm), ), ); } class _CalendarPicker extends ConsumerStatefulWidget { const _CalendarPicker(); @override ConsumerState<_CalendarPicker> createState() => _CalendarPickerState(); } class _CalendarPickerState extends ConsumerState<_CalendarPicker> { late DateTime _picked = ref.read(selectedDateProvider); late DateTime _month = DateTime(_picked.year, _picked.month); void _shift(int by) => setState(() => _month = DateTime(_month.year, _month.month + by)); void _pick(DateTime d) => setState(() { _picked = d; _month = DateTime(d.year, d.month); }); void _confirm() { ref.read(selectedDateProvider.notifier).set(_picked); Navigator.of(context).pop(); } @override Widget build(BuildContext context) { final today = dateOnly(ref.read(apiProvider).now); final days = ref.watch(monthProvider((_month.year, _month.month))); final byDay = { for (final d in days.value ?? const []) d.date.day: d, }; final pickedInfo = ref .watch(monthProvider((_picked.year, _picked.month))) .value ?.where((d) => sameDay(d.date, _picked)) .firstOrNull; final first = DateTime(_month.year, _month.month); final lead = first.weekday - 1; final count = DateTime(_month.year, _month.month + 1, 0).day; final rows = ((lead + count) / 7).ceil(); return CallbackShortcuts( bindings: { const SingleActivator(LogicalKeyboardKey.arrowLeft): () => _pick(addDays(_picked, -1)), const SingleActivator(LogicalKeyboardKey.arrowRight): () => _pick(addDays(_picked, 1)), const SingleActivator(LogicalKeyboardKey.arrowUp): () => _pick(addDays(_picked, -7)), const SingleActivator(LogicalKeyboardKey.arrowDown): () => _pick(addDays(_picked, 7)), }, child: Focus( autofocus: true, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ const SizedBox(width: S.s4), Expanded( child: Text(monthYear(_month), style: ts(18, w: w7)), ), if (!sameDay(DateTime(today.year, today.month), _month) || !sameDay(_picked, today)) Padding( padding: const EdgeInsets.only(right: S.s4), child: Pressable( onTap: () => _pick(today), radius: R.pill, builder: (_, hover) => AnimatedContainer( duration: const Duration(milliseconds: 140), height: 32, padding: const EdgeInsets.symmetric(horizontal: S.s12), alignment: Alignment.center, decoration: BoxDecoration( color: hover ? C.chip : Colors.transparent, borderRadius: BorderRadius.circular(R.pill), ), child: Text( 'Today', style: ts(13, w: w6, c: C.navy), ), ), ), ), RoundIcon( Icons.chevron_left_rounded, label: 'Previous month', size: 36, onTap: () => _shift(-1), ), const SizedBox(width: S.s6), RoundIcon( Icons.chevron_right_rounded, label: 'Next month', size: 36, onTap: () => _shift(1), ), ], ), const SizedBox(height: S.s14), Row( children: [ for (final w in const ['M', 'T', 'W', 'T', 'F', 'S', 'S']) Expanded( child: Center( child: Text( w, style: ts(12, w: w6, c: C.muted), ), ), ), ], ), const SizedBox(height: S.s6), for (var r = 0; r < rows; r++) Row( children: [ for (var c = 0; c < 7; c++) Expanded( child: _cell(r * 7 + c - lead + 1, count, byDay, today), ), ], ), const SizedBox(height: S.s12), _Summary(date: _picked, info: pickedInfo), const SizedBox(height: S.s14), BigButton( 'Show results for ${dateShort(_picked)}', onPressed: _confirm, ), ], ), ), ); } Widget _cell(int day, int count, Map byDay, DateTime today) { if (day < 1 || day > count) return const SizedBox(height: 48); final date = DateTime(_month.year, _month.month, day); final sel = sameDay(date, _picked); final isToday = sameDay(date, today); final info = byDay[day]; final out = info?.published == true; final win = info?.hasWin == true; final status = [ if (out) 'results published', if (win) 'winning tickets', ].join(', '); return Pressable( onTap: () => _pick(date), label: status.isEmpty ? dateLong(date) : '${dateLong(date)}, $status', radius: R.md, builder: (_, hover) => Padding( padding: const EdgeInsets.all(S.s2), child: AnimatedContainer( duration: const Duration(milliseconds: 120), height: 44, decoration: BoxDecoration( color: sel ? C.navy : (hover ? C.chip : Colors.transparent), borderRadius: BorderRadius.circular(R.md), border: !sel && isToday ? Border.all(color: C.navy, width: 1.4) : null, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '$day', style: ts( 14, w: sel || isToday ? w7 : w5, c: sel ? Colors.white : C.ink, ), ), const SizedBox(height: S.s4), SizedBox( height: 4, child: Row( mainAxisSize: MainAxisSize.min, children: [ if (out) Container( width: 12, height: 4, decoration: BoxDecoration( color: sel ? C.softOnNavy : C.green, borderRadius: BorderRadius.circular(2), ), ), if (out && win) const SizedBox(width: 3), if (win) Container( width: 4, height: 4, decoration: const BoxDecoration( color: C.gold, shape: BoxShape.circle, ), ), ], ), ), ], ), ), ), ); } } class _Summary extends StatelessWidget { const _Summary({required this.date, required this.info}); final DateTime date; final DrawInfo? info; @override Widget build(BuildContext context) { final out = info?.published == true; final win = info?.hasWin == true; final (icon, color, text) = !out ? (Icons.schedule_rounded, C.goldText, 'Results not published yet') : win ? (Icons.star_rounded, C.goldText, 'Results out, with winning tickets') : (Icons.check_circle_rounded, C.green, 'Results out'); return Container( padding: const EdgeInsets.all(S.s12), decoration: BoxDecoration( color: C.bg, borderRadius: BorderRadius.circular(R.input), ), child: Row( children: [ Icon(icon, size: 20, color: color), const SizedBox(width: S.s10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( out && info!.code != null ? '${info!.scheme ?? 'Draw'} ${info!.code}' : dateLong(date), maxLines: 1, overflow: TextOverflow.ellipsis, style: ts(14, w: w6), ), const SizedBox(height: S.s2), Text(text, style: ts(12, c: C.muted)), ], ), ), ], ), ); } }