feat: add calendar view without interaction

This commit is contained in:
Freek van de Ven 2024-07-05 16:09:16 +02:00 committed by Bart Ribbers
parent fa1d9504f5
commit bb01592f54
5 changed files with 452 additions and 5 deletions

View file

@ -16,6 +16,7 @@ class AvailabilityOptions {
this.primaryButtonBuilder = DefaultPrimaryButton.builder,
this.textButtonBuilder = DefaultTextButton.builder,
this.spacing = const AvailabilitySpacing(),
this.colors = const AvailabilityColors(),
AvailabilityDataInterface? dataInterface,
}) : dataInterface = dataInterface ?? LocalAvailabilityDataInterface();
@ -40,6 +41,9 @@ class AvailabilityOptions {
/// The spacing between elements
final AvailabilitySpacing spacing;
/// The colors used in the userstory
final AvailabilityColors colors;
}
/// All configurable paddings and whitespaces withing the userstory
@ -57,6 +61,46 @@ class AvailabilitySpacing {
final double sidePadding;
}
/// Contains all the customizable colors for the availability userstory
///
/// If colors are not provided the colors will be taken from the theme
class AvailabilityColors {
/// Constructor for the AvailabilityColors
///
/// If a color is not provided the color will be taken from the theme
const AvailabilityColors({
this.selectedDayColor,
this.blankDayColor,
this.outsideMonthTextColor,
this.textDarkColor,
this.textLightColor,
this.templateColors,
});
/// The color of the text for the days that are not in the current month
final Color? outsideMonthTextColor;
/// The background color of the days that are selected in the calendar
final Color? selectedDayColor;
/// The background color of the days in the current month
/// that have no availability and are not selected
final Color? blankDayColor;
/// The color of the text in the calendar when the background has a dark color
/// This is used to make sure the text is readable on dark backgrounds
/// If not provided the text color will be white
final Color? textLightColor;
/// The color of the text in the calendar when the background has a light
/// color. This is used to make sure the text is readable on light backgrounds
/// If not provided the text color will be the theme's text color
final Color? textDarkColor;
/// The colors that are used for the templates
final List<Color>? templateColors;
}
/// Builder definition for providing a base screen surrounding each page
typedef BaseScreenBuilder = Widget Function(
BuildContext context,

View file

@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
/// Class that holds all translatable strings for the availability userstory
class AvailabilityTranslations {
/// AvailabilityTranslations constructor where everything is required.
@ -14,6 +16,8 @@ class AvailabilityTranslations {
required this.templateLegendTitle,
required this.overviewScreenTitle,
required this.createTemplateButton,
required this.monthYearFormatter,
required this.weekDayAbbreviatedFormatter,
});
/// AvailabilityTranslations constructor where everything is optional.
@ -24,6 +28,8 @@ class AvailabilityTranslations {
this.templateLegendTitle = "Templates",
this.createTemplateButton = "Create a new template",
this.overviewScreenTitle = "Availability",
this.monthYearFormatter = _defaultMonthYearFormatter,
this.weekDayAbbreviatedFormatter = _defaultWeekDayAbbreviatedFormatter,
});
/// The title shown above the calendar
@ -40,4 +46,51 @@ class AvailabilityTranslations {
/// The label on the button to go to the tempalte creation page
final String createTemplateButton;
/// Gets the month and year formatted as a string
///
/// The default implementation is `MonthName Year` in english
final String Function(BuildContext, DateTime) monthYearFormatter;
/// Gets the abbreviated name of a weekday
///
/// The default implementation is the first 2 letters of
/// the weekday in english
final String Function(BuildContext, DateTime) weekDayAbbreviatedFormatter;
}
String _defaultWeekDayAbbreviatedFormatter(
BuildContext context,
DateTime date,
) =>
_getWeekDayAbbreviation(date.weekday);
String _defaultMonthYearFormatter(BuildContext context, DateTime date) =>
"${_getMonthName(date.month)} ${date.year}";
String _getWeekDayAbbreviation(int weekday) => switch (weekday) {
1 => "Mo",
2 => "Tu",
3 => "We",
4 => "Th",
5 => "Fr",
6 => "Sa",
7 => "Su",
_ => "",
};
String _getMonthName(int month) => switch (month) {
1 => "January",
2 => "February",
3 => "March",
4 => "April",
5 => "May",
6 => "June",
7 => "July",
8 => "August",
9 => "September",
10 => "October",
11 => "November",
12 => "December",
_ => "",
};

View file

@ -1,8 +1,9 @@
import "package:flutter/material.dart";
import "package:flutter_availability/src/ui/widgets/calendar.dart";
import "package:flutter_availability/src/util/scope.dart";
///
class AvailabilityOverview extends StatelessWidget {
class AvailabilityOverview extends StatefulWidget {
///
const AvailabilityOverview({
required this.onEditDateRange,
@ -20,6 +21,13 @@ class AvailabilityOverview extends StatelessWidget {
/// Callback invoked when a user attempts to go back
final VoidCallback onBack;
@override
State<AvailabilityOverview> createState() => _AvailabilityOverviewState();
}
class _AvailabilityOverviewState extends State<AvailabilityOverview> {
DateTime _selectedDate = DateTime.now();
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
@ -35,9 +43,13 @@ class AvailabilityOverview extends StatelessWidget {
),
);
const calendar = SizedBox(
height: 320,
child: Placeholder(),
var calendar = CalendarView(
month: _selectedDate,
onMonthChanged: (month) {
setState(() {
_selectedDate = month;
});
},
);
const templateLegend = SizedBox(
@ -48,7 +60,9 @@ class AvailabilityOverview extends StatelessWidget {
var startEditButton = options.primaryButtonBuilder(
context,
() {
onEditDateRange(DateTimeRange(start: DateTime(1), end: DateTime(2)));
widget.onEditDateRange(
DateTimeRange(start: DateTime(1), end: DateTime(2)),
);
},
Text(translations.editAvailabilityButton),
);

View file

@ -0,0 +1,107 @@
import "package:flutter/material.dart";
import "package:flutter_availability/src/ui/widgets/calendar_grid.dart";
import "package:flutter_availability/src/util/scope.dart";
///
class CalendarView extends StatelessWidget {
///
const CalendarView({
required this.month,
required this.onMonthChanged,
super.key,
});
///
final DateTime month;
///
final void Function(DateTime month) onMonthChanged;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var availabilityScope = AvailabilityScope.of(context);
var options = availabilityScope.options;
var translations = options.translations;
var monthDateSelector = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () {
onMonthChanged(DateTime(month.year, month.month - 1));
},
),
const SizedBox(width: 44),
Text(
translations.monthYearFormatter(context, month),
style: theme.textTheme.bodyLarge,
),
const SizedBox(width: 44),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: () {
onMonthChanged(DateTime(month.year, 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,
),
],
);
return Column(
children: [
monthDateSelector,
const Divider(),
calendarGrid,
],
);
}
}

View file

@ -0,0 +1,229 @@
import "package:flutter/material.dart";
import "package:flutter_availability/flutter_availability.dart";
import "package:flutter_availability/src/util/scope.dart";
///
class CalendarGrid extends StatelessWidget {
///
const CalendarGrid({
required this.month,
required this.days,
super.key,
});
/// The current month to display
final DateTime month;
/// A list of days that need to be displayed differently
final List<CalendarDay> days;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var textTheme = theme.textTheme;
var colorScheme = theme.colorScheme;
var availabilityScope = AvailabilityScope.of(context);
var options = availabilityScope.options;
var colors = options.colors;
var translations = options.translations;
var calendarDays = _generateCalendarDays(month, days, colors, colorScheme);
// get the names of the days of the week
var dayNames = List.generate(7, (index) {
var day = DateTime(2024, 7, 8 + index); // this is a monday
return translations.weekDayAbbreviatedFormatter(context, day);
});
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),
GridView.builder(
shrinkWrap: true,
itemCount: calendarDays.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemBuilder: (context, index) {
var day = calendarDays[index];
var textColor = day.outsideMonth
? colors.outsideMonthTextColor ?? colorScheme.onSurface
: _getTextColor(
day.color,
colors.textLightColor ?? Colors.white,
colors.textDarkColor,
);
var textStyle = textTheme.bodyLarge?.copyWith(color: textColor);
return GestureDetector(
onTap: () {
// Handle day tap here
},
child: DecoratedBox(
decoration: BoxDecoration(
color: day.outsideMonth ? Colors.transparent : day.color,
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: day.isSelected
? colorScheme.primary
: Colors.transparent,
),
),
child: Stack(
children: [
Center(
child: Text(day.date.day.toString(), style: textStyle),
),
if (day.templateDeviation) ...[
Positioned(
right: 4,
child: Text("*", style: textStyle),
),
],
],
),
),
);
},
),
],
);
}
}
/// A Special day in the calendar that needs to be displayed differently
class CalendarDay {
///
const CalendarDay({
required this.date,
required this.isSelected,
required this.color,
required this.templateDeviation,
this.outsideMonth = false,
});
/// The date of the day
final DateTime date;
/// Whether the day is selected or not
final bool isSelected;
/// The color of the day
final Color color;
/// Whether there is an availability on this day and it deviates from the
/// used template
final bool templateDeviation;
/// Whether the day is outside of the current month
final bool outsideMonth;
/// Creates a copy of the current day with the provided values
CalendarDay copyWith({
DateTime? date,
bool? isSelected,
Color? color,
bool? templateDeviation,
bool? outsideMonth,
}) =>
CalendarDay(
date: date ?? this.date,
isSelected: isSelected ?? this.isSelected,
color: color ?? this.color,
templateDeviation: templateDeviation ?? this.templateDeviation,
outsideMonth: outsideMonth ?? this.outsideMonth,
);
}
List<CalendarDay> _generateCalendarDays(
DateTime month,
List<CalendarDay> days,
AvailabilityColors colors,
ColorScheme colorScheme,
) {
var firstDayOfMonth = DateTime(month.year, month.month, 1);
var lastDayOfMonth = DateTime(month.year, month.month + 1, 0);
var daysInMonth = lastDayOfMonth.day;
var startWeekday = firstDayOfMonth.weekday;
var endWeekday = lastDayOfMonth.weekday;
var calendarDays = <CalendarDay>[];
// Add days from the previous month
for (var i = 0; i < startWeekday - 1; i++) {
var prevDay =
firstDayOfMonth.subtract(Duration(days: startWeekday - 1 - i));
calendarDays.add(
CalendarDay(
date: prevDay,
isSelected: false,
color: Colors.transparent,
templateDeviation: false,
outsideMonth: true,
),
);
}
// Add days of the current month
for (var i = 1; i <= daysInMonth; i++) {
var day = DateTime(month.year, month.month, i);
var specialDay = days.firstWhere(
(d) =>
d.date.day == i &&
d.date.month == month.month &&
d.date.year == month.year,
orElse: () => CalendarDay(
date: day,
isSelected: false,
color: colors.blankDayColor ?? colorScheme.surfaceDim,
templateDeviation: false,
),
);
// if the day is selected we need to change the color
specialDay = specialDay.copyWith(
color: specialDay.isSelected
? colors.selectedDayColor ?? colorScheme.primaryFixedDim
: null,
);
calendarDays.add(specialDay);
}
// Add days from the next month
for (var i = endWeekday; i < 7; i++) {
var nextDay = lastDayOfMonth.add(Duration(days: i - endWeekday + 1));
calendarDays.add(
CalendarDay(
date: nextDay,
isSelected: false,
color: Colors.transparent,
templateDeviation: false,
outsideMonth: true,
),
);
}
return calendarDays;
}
Color? _getTextColor(
Color backgroundColor,
Color lightColor,
Color? darkColor,
) =>
backgroundColor.computeLuminance() > 0.5 ? darkColor : lightColor;