feat: add period selection to the calendar view

There are also some small refactors for the UI of the calendar included
This commit is contained in:
Freek van de Ven 2024-07-06 13:53:56 +02:00 committed by Bart Ribbers
parent f840d1b019
commit a22c8ed69e
4 changed files with 148 additions and 79 deletions

View file

@ -111,6 +111,6 @@ typedef BaseScreenBuilder = Widget Function(
/// Builder definition for providing a button implementation /// Builder definition for providing a button implementation
typedef ButtonBuilder = Widget Function( typedef ButtonBuilder = Widget Function(
BuildContext context, BuildContext context,
FutureOr<void>? Function() onPressed, FutureOr<void>? Function()? onPressed,
Widget child, Widget child,
); );

View file

@ -27,6 +27,7 @@ class AvailabilityOverview extends StatefulWidget {
class _AvailabilityOverviewState extends State<AvailabilityOverview> { class _AvailabilityOverviewState extends State<AvailabilityOverview> {
DateTime _selectedDate = DateTime.now(); DateTime _selectedDate = DateTime.now();
DateTimeRange? _selectedRange;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -50,6 +51,11 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
_selectedDate = month; _selectedDate = month;
}); });
}, },
onEditDateRange: (range) {
setState(() {
_selectedRange = range;
});
},
); );
const templateLegend = SizedBox( const templateLegend = SizedBox(
@ -57,13 +63,18 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
child: Placeholder(), 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( var startEditButton = options.primaryButtonBuilder(
context, context,
() { onButtonPress,
widget.onEditDateRange(
DateTimeRange(start: DateTime(1), end: DateTime(2)),
);
},
Text(translations.editAvailabilityButton), Text(translations.editAvailabilityButton),
); );

View file

@ -1,13 +1,15 @@
import "package:flutter/material.dart"; 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/ui/widgets/calendar_grid.dart";
import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability/src/util/scope.dart";
/// ///
class CalendarView extends StatelessWidget { class CalendarView extends StatefulWidget {
/// ///
const CalendarView({ const CalendarView({
required this.month, required this.month,
required this.onMonthChanged, required this.onMonthChanged,
required this.onEditDateRange,
super.key, super.key,
}); });
@ -17,6 +19,51 @@ class CalendarView extends StatelessWidget {
/// ///
final void Function(DateTime month) onMonthChanged; 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<CalendarView> createState() => _CalendarViewState();
}
class _CalendarViewState extends State<CalendarView> {
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
@ -28,80 +75,74 @@ class CalendarView extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
IconButton( IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.chevron_left), icon: const Icon(Icons.chevron_left),
onPressed: () { onPressed: () {
onMonthChanged(DateTime(month.year, month.month - 1)); widget.onMonthChanged(
DateTime(widget.month.year, widget.month.month - 1),
);
}, },
), ),
const SizedBox(width: 44), const SizedBox(width: 44),
Text( SizedBox(
translations.monthYearFormatter(context, month), width: _calculateTextWidthOfLongestMonth(context, translations),
style: theme.textTheme.bodyLarge, child: Text(
translations.monthYearFormatter(context, widget.month),
style: theme.textTheme.titleMedium,
textAlign: TextAlign.center,
),
), ),
const SizedBox(width: 44), const SizedBox(width: 44),
IconButton( IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.chevron_right), icon: const Icon(Icons.chevron_right),
onPressed: () { onPressed: () {
onMonthChanged(DateTime(month.year, month.month + 1)); widget.onMonthChanged(
DateTime(widget.month.year, widget.month.month + 1),
);
}, },
), ),
], ],
); );
var calendarGrid = CalendarGrid( var calendarGrid = CalendarGrid(
month: month, month: widget.month,
days: [ days: const [],
CalendarDay( onDayTap: onTapDate,
date: DateTime(month.year, month.month, 3), selectedRange: _selectedRange,
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,
),
],
); );
return Column( return Column(
children: [ children: [
monthDateSelector, monthDateSelector,
const Divider(), const Divider(),
const SizedBox(height: 20),
calendarGrid, 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;
}

View file

@ -8,6 +8,8 @@ class CalendarGrid extends StatelessWidget {
const CalendarGrid({ const CalendarGrid({
required this.month, required this.month,
required this.days, required this.days,
required this.onDayTap,
required this.selectedRange,
super.key, super.key,
}); });
@ -17,6 +19,12 @@ class CalendarGrid extends StatelessWidget {
/// A list of days that need to be displayed differently /// A list of days that need to be displayed differently
final List<CalendarDay> days; final List<CalendarDay> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
@ -26,7 +34,8 @@ class CalendarGrid extends StatelessWidget {
var options = availabilityScope.options; var options = availabilityScope.options;
var colors = options.colors; var colors = options.colors;
var translations = options.translations; 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 // get the names of the days of the week
var dayNames = List.generate(7, (index) { var dayNames = List.generate(7, (index) {
@ -34,25 +43,30 @@ class CalendarGrid extends StatelessWidget {
return translations.weekDayAbbreviatedFormatter(context, day); 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( return Column(
children: [ children: [
Row( calendarDaysRow,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: dayNames
.map(
(day) => Expanded(
child: Center(
child: Text(
day,
style: textTheme.bodyLarge,
),
),
),
)
.toList(),
),
const SizedBox(height: 4.0),
GridView.builder( GridView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true, shrinkWrap: true,
itemCount: calendarDays.length, itemCount: calendarDays.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
@ -72,9 +86,7 @@ class CalendarGrid extends StatelessWidget {
var textStyle = textTheme.bodyLarge?.copyWith(color: textColor); var textStyle = textTheme.bodyLarge?.copyWith(color: textColor);
return GestureDetector( return GestureDetector(
onTap: () { onTap: () => onDayTap(day.date),
// Handle day tap here
},
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: day.outsideMonth ? Colors.transparent : day.color, color: day.outsideMonth ? Colors.transparent : day.color,
@ -154,6 +166,7 @@ class CalendarDay {
List<CalendarDay> _generateCalendarDays( List<CalendarDay> _generateCalendarDays(
DateTime month, DateTime month,
List<CalendarDay> days, List<CalendarDay> days,
DateTimeRange? selectedRange,
AvailabilityColors colors, AvailabilityColors colors,
ColorScheme colorScheme, ColorScheme colorScheme,
) { ) {
@ -195,11 +208,15 @@ List<CalendarDay> _generateCalendarDays(
templateDeviation: false, 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( specialDay = specialDay.copyWith(
color: specialDay.isSelected color: dayIsSelected
? colors.selectedDayColor ?? colorScheme.primaryFixedDim ? colors.selectedDayColor ?? colorScheme.primaryFixedDim
: null, : null,
templateDeviation: dayIsSelected ? false : null,
); );
calendarDays.add(specialDay); calendarDays.add(specialDay);
} }