diff --git a/packages/flutter_availability/lib/src/config/availability_options.dart b/packages/flutter_availability/lib/src/config/availability_options.dart index f2a674d..044215f 100644 --- a/packages/flutter_availability/lib/src/config/availability_options.dart +++ b/packages/flutter_availability/lib/src/config/availability_options.dart @@ -111,6 +111,6 @@ typedef BaseScreenBuilder = Widget Function( /// Builder definition for providing a button implementation typedef ButtonBuilder = Widget Function( BuildContext context, - FutureOr? Function() onPressed, + FutureOr? Function()? onPressed, Widget child, ); diff --git a/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart b/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart index 87d8705..acedbe3 100644 --- a/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart +++ b/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart @@ -27,6 +27,7 @@ class AvailabilityOverview extends StatefulWidget { class _AvailabilityOverviewState extends State { DateTime _selectedDate = DateTime.now(); + DateTimeRange? _selectedRange; @override Widget build(BuildContext context) { @@ -50,6 +51,11 @@ class _AvailabilityOverviewState extends State { _selectedDate = month; }); }, + onEditDateRange: (range) { + setState(() { + _selectedRange = range; + }); + }, ); const templateLegend = SizedBox( @@ -57,13 +63,18 @@ class _AvailabilityOverviewState extends State { child: Placeholder(), ); + // if there is no range selected we want to disable the button + var onButtonPress = _selectedRange == null + ? null + : () { + widget.onEditDateRange( + DateTimeRange(start: DateTime(1), end: DateTime(2)), + ); + }; + var startEditButton = options.primaryButtonBuilder( context, - () { - widget.onEditDateRange( - DateTimeRange(start: DateTime(1), end: DateTime(2)), - ); - }, + onButtonPress, Text(translations.editAvailabilityButton), ); diff --git a/packages/flutter_availability/lib/src/ui/widgets/calendar.dart b/packages/flutter_availability/lib/src/ui/widgets/calendar.dart index 3a89476..a18576e 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/calendar.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/calendar.dart @@ -1,13 +1,15 @@ import "package:flutter/material.dart"; +import "package:flutter_availability/src/config/availability_translations.dart"; import "package:flutter_availability/src/ui/widgets/calendar_grid.dart"; import "package:flutter_availability/src/util/scope.dart"; /// -class CalendarView extends StatelessWidget { +class CalendarView extends StatefulWidget { /// const CalendarView({ required this.month, required this.onMonthChanged, + required this.onEditDateRange, super.key, }); @@ -17,6 +19,51 @@ class CalendarView extends StatelessWidget { /// final void Function(DateTime month) onMonthChanged; + /// Callback for when the date range is edited by the user + final void Function(DateTimeRange? range) onEditDateRange; + + @override + State createState() => _CalendarViewState(); +} + +class _CalendarViewState extends State { + DateTimeRange? _selectedRange; + + void onTapDate(DateTime day) { + // if there is already a range selected, with a single date and the date + //that is selected is after it we extend the range + if (_selectedRange != null && + day.isAfter(_selectedRange!.start) && + _selectedRange!.start == _selectedRange!.end) { + setState(() { + _selectedRange = DateTimeRange( + start: _selectedRange!.start, + end: day, + ); + }); + widget.onEditDateRange(_selectedRange); + return; + } + + // if select the already selected date we want to clear the range + if (_selectedRange != null && + day.isAtSameMomentAs(_selectedRange!.start) && + day.isAtSameMomentAs(_selectedRange!.end)) { + setState(() { + _selectedRange = null; + }); + widget.onEditDateRange(_selectedRange); + return; + } + + // if there is already a range selected we want to clear + //it and start a new one + setState(() { + _selectedRange = DateTimeRange(start: day, end: day); + }); + widget.onEditDateRange(_selectedRange); + } + @override Widget build(BuildContext context) { var theme = Theme.of(context); @@ -28,80 +75,74 @@ class CalendarView extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( + padding: EdgeInsets.zero, icon: const Icon(Icons.chevron_left), onPressed: () { - onMonthChanged(DateTime(month.year, month.month - 1)); + widget.onMonthChanged( + DateTime(widget.month.year, widget.month.month - 1), + ); }, ), const SizedBox(width: 44), - Text( - translations.monthYearFormatter(context, month), - style: theme.textTheme.bodyLarge, + SizedBox( + width: _calculateTextWidthOfLongestMonth(context, translations), + child: Text( + translations.monthYearFormatter(context, widget.month), + style: theme.textTheme.titleMedium, + textAlign: TextAlign.center, + ), ), const SizedBox(width: 44), IconButton( + padding: EdgeInsets.zero, icon: const Icon(Icons.chevron_right), onPressed: () { - onMonthChanged(DateTime(month.year, month.month + 1)); + widget.onMonthChanged( + DateTime(widget.month.year, widget.month.month + 1), + ); }, ), ], ); var calendarGrid = CalendarGrid( - month: month, - days: [ - CalendarDay( - date: DateTime(month.year, month.month, 3), - isSelected: false, - color: Colors.red, - templateDeviation: false, - ), - CalendarDay( - date: DateTime(month.year, month.month, 4), - isSelected: false, - color: Colors.red, - templateDeviation: true, - ), - CalendarDay( - date: DateTime(month.year, month.month, 10), - isSelected: false, - color: Colors.blue, - templateDeviation: false, - ), - CalendarDay( - date: DateTime(month.year, month.month, 11), - isSelected: false, - color: Colors.black, - templateDeviation: true, - ), - CalendarDay( - date: DateTime(month.year, month.month, 12), - isSelected: false, - color: Colors.white, - templateDeviation: true, - ), - CalendarDay( - date: DateTime(month.year, month.month, 13), - isSelected: true, - color: Colors.green, - templateDeviation: false, - ), - CalendarDay( - date: DateTime(month.year, month.month, 14), - isSelected: true, - color: Colors.green, - templateDeviation: true, - ), - ], + month: widget.month, + days: const [], + onDayTap: onTapDate, + selectedRange: _selectedRange, ); return Column( children: [ monthDateSelector, const Divider(), + const SizedBox(height: 20), calendarGrid, ], ); } } + +/// loops through all the months of a year and get the width of the +/// longest month, +/// this is used to make sure the month selector is always the same width +double _calculateTextWidthOfLongestMonth( + BuildContext context, + AvailabilityTranslations translations, +) { + var longestMonth = List.generate(12, (index) { + var month = DateTime(2024, index + 1); + return translations.monthYearFormatter(context, month); + }).reduce( + (value, element) => value.length > element.length ? value : element, + ); + // now we calculate the width of the longest month + var textPainter = TextPainter( + text: TextSpan( + text: longestMonth, + style: Theme.of(context).textTheme.titleMedium, + ), + textDirection: TextDirection.ltr, + )..layout(); + return textPainter.width; +} diff --git a/packages/flutter_availability/lib/src/ui/widgets/calendar_grid.dart b/packages/flutter_availability/lib/src/ui/widgets/calendar_grid.dart index 2fd2d4e..bf5c22c 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/calendar_grid.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/calendar_grid.dart @@ -8,6 +8,8 @@ class CalendarGrid extends StatelessWidget { const CalendarGrid({ required this.month, required this.days, + required this.onDayTap, + required this.selectedRange, super.key, }); @@ -17,6 +19,12 @@ class CalendarGrid extends StatelessWidget { /// A list of days that need to be displayed differently final List days; + /// A callback that is called when a day is tapped + final void Function(DateTime) onDayTap; + + /// The selected range of dates + final DateTimeRange? selectedRange; + @override Widget build(BuildContext context) { var theme = Theme.of(context); @@ -26,7 +34,8 @@ class CalendarGrid extends StatelessWidget { var options = availabilityScope.options; var colors = options.colors; var translations = options.translations; - var calendarDays = _generateCalendarDays(month, days, colors, colorScheme); + var calendarDays = + _generateCalendarDays(month, days, selectedRange, colors, colorScheme); // get the names of the days of the week var dayNames = List.generate(7, (index) { @@ -34,25 +43,30 @@ class CalendarGrid extends StatelessWidget { return translations.weekDayAbbreviatedFormatter(context, day); }); + var calendarDaysRow = GridView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: 7, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + crossAxisSpacing: 12, + mainAxisSpacing: 0, + ), + itemBuilder: (context, index) { + var day = dayNames[index]; + return Text( + day, + style: textTheme.bodyLarge, + textAlign: TextAlign.center, + ); + }, + ); + return Column( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: dayNames - .map( - (day) => Expanded( - child: Center( - child: Text( - day, - style: textTheme.bodyLarge, - ), - ), - ), - ) - .toList(), - ), - const SizedBox(height: 4.0), + calendarDaysRow, GridView.builder( + padding: EdgeInsets.zero, shrinkWrap: true, itemCount: calendarDays.length, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( @@ -72,9 +86,7 @@ class CalendarGrid extends StatelessWidget { var textStyle = textTheme.bodyLarge?.copyWith(color: textColor); return GestureDetector( - onTap: () { - // Handle day tap here - }, + onTap: () => onDayTap(day.date), child: DecoratedBox( decoration: BoxDecoration( color: day.outsideMonth ? Colors.transparent : day.color, @@ -154,6 +166,7 @@ class CalendarDay { List _generateCalendarDays( DateTime month, List days, + DateTimeRange? selectedRange, AvailabilityColors colors, ColorScheme colorScheme, ) { @@ -195,11 +208,15 @@ List _generateCalendarDays( templateDeviation: false, ), ); - // if the day is selected we need to change the color + var dayIsSelected = selectedRange != null && + !day.isBefore(selectedRange.start) && + !day.isAfter(selectedRange.end); + // if the day is selected we need to change the color and remove the marking specialDay = specialDay.copyWith( - color: specialDay.isSelected + color: dayIsSelected ? colors.selectedDayColor ?? colorScheme.primaryFixedDim : null, + templateDeviation: dayIsSelected ? false : null, ); calendarDays.add(specialDay); }