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
|
/// 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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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(),
|
||||||
);
|
);
|
||||||
|
|
||||||
var startEditButton = options.primaryButtonBuilder(
|
// if there is no range selected we want to disable the button
|
||||||
context,
|
var onButtonPress = _selectedRange == null
|
||||||
() {
|
? null
|
||||||
|
: () {
|
||||||
widget.onEditDateRange(
|
widget.onEditDateRange(
|
||||||
DateTimeRange(start: DateTime(1), end: DateTime(2)),
|
DateTimeRange(start: DateTime(1), end: DateTime(2)),
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
|
|
||||||
|
var startEditButton = options.primaryButtonBuilder(
|
||||||
|
context,
|
||||||
|
onButtonPress,
|
||||||
Text(translations.editAvailabilityButton),
|
Text(translations.editAvailabilityButton),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Column(
|
var calendarDaysRow = GridView.builder(
|
||||||
children: [
|
padding: EdgeInsets.zero,
|
||||||
Row(
|
shrinkWrap: true,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
itemCount: 7,
|
||||||
children: dayNames
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
.map(
|
crossAxisCount: 7,
|
||||||
(day) => Expanded(
|
crossAxisSpacing: 12,
|
||||||
child: Center(
|
mainAxisSpacing: 0,
|
||||||
child: Text(
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
var day = dayNames[index];
|
||||||
|
return Text(
|
||||||
day,
|
day,
|
||||||
style: textTheme.bodyLarge,
|
style: textTheme.bodyLarge,
|
||||||
),
|
textAlign: TextAlign.center,
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
)
|
);
|
||||||
.toList(),
|
|
||||||
),
|
return Column(
|
||||||
const SizedBox(height: 4.0),
|
children: [
|
||||||
|
calendarDaysRow,
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue