mirror of
https://github.com/Iconica-Development/flutter_availability.git
synced 2025-05-19 13:13:44 +02:00
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:
parent
f840d1b019
commit
a22c8ed69e
4 changed files with 148 additions and 79 deletions
|
@ -111,6 +111,6 @@ typedef BaseScreenBuilder = Widget Function(
|
|||
/// Builder definition for providing a button implementation
|
||||
typedef ButtonBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
FutureOr<void>? Function() onPressed,
|
||||
FutureOr<void>? Function()? onPressed,
|
||||
Widget child,
|
||||
);
|
||||
|
|
|
@ -27,6 +27,7 @@ class AvailabilityOverview extends StatefulWidget {
|
|||
|
||||
class _AvailabilityOverviewState extends State<AvailabilityOverview> {
|
||||
DateTime _selectedDate = DateTime.now();
|
||||
DateTimeRange? _selectedRange;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -50,6 +51,11 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
|
|||
_selectedDate = month;
|
||||
});
|
||||
},
|
||||
onEditDateRange: (range) {
|
||||
setState(() {
|
||||
_selectedRange = range;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const templateLegend = SizedBox(
|
||||
|
@ -57,13 +63,18 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
|
|||
child: Placeholder(),
|
||||
);
|
||||
|
||||
var startEditButton = options.primaryButtonBuilder(
|
||||
context,
|
||||
() {
|
||||
// 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,
|
||||
onButtonPress,
|
||||
Text(translations.editAvailabilityButton),
|
||||
);
|
||||
|
||||
|
|
|
@ -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<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
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
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);
|
||||
});
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: dayNames
|
||||
.map(
|
||||
(day) => Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 4.0),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
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<CalendarDay> _generateCalendarDays(
|
||||
DateTime month,
|
||||
List<CalendarDay> days,
|
||||
DateTimeRange? selectedRange,
|
||||
AvailabilityColors colors,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
|
@ -195,11 +208,15 @@ List<CalendarDay> _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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue