mirror of
https://github.com/Iconica-Development/flutter_availability.git
synced 2025-05-19 05:03:44 +02:00
feat: add calendar view without interaction
This commit is contained in:
parent
fa1d9504f5
commit
bb01592f54
5 changed files with 452 additions and 5 deletions
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
_ => "",
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
|
|
107
packages/flutter_availability/lib/src/ui/widgets/calendar.dart
Normal file
107
packages/flutter_availability/lib/src/ui/widgets/calendar.dart
Normal 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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
Loading…
Reference in a new issue