diff --git a/example/lib/pages/mobile/mobile_home_page.dart b/example/lib/pages/mobile/mobile_home_page.dart index 640dde95..4498c10c 100644 --- a/example/lib/pages/mobile/mobile_home_page.dart +++ b/example/lib/pages/mobile/mobile_home_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../extension.dart'; import '../day_view_page.dart'; import '../month_view_page.dart'; +import '../multi_day_view_page.dart'; import '../week_view_page.dart'; class MobileHomePage extends StatelessWidget { @@ -35,6 +36,13 @@ class MobileHomePage extends StatelessWidget { onPressed: () => context.pushRoute(WeekViewDemo()), child: Text("Week View"), ), + SizedBox( + height: 20, + ), + ElevatedButton( + onPressed: () => context.pushRoute(MultiDayViewDemo()), + child: Text("Multi-Day View"), + ), ], ), ), diff --git a/example/lib/pages/multi_day_view_page.dart b/example/lib/pages/multi_day_view_page.dart new file mode 100644 index 00000000..579e5ab2 --- /dev/null +++ b/example/lib/pages/multi_day_view_page.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +import '../enumerations.dart'; +import '../extension.dart'; +import '../widgets/responsive_widget.dart'; +import '../widgets/multi_day_view_widget.dart'; +import 'create_event_page.dart'; +import 'web/web_home_page.dart'; + +class MultiDayViewDemo extends StatefulWidget { + const MultiDayViewDemo({super.key}); + + @override + _MultiDayViewDemoState createState() => _MultiDayViewDemoState(); +} + +class _MultiDayViewDemoState extends State { + @override + Widget build(BuildContext context) { + return ResponsiveWidget( + webWidget: WebHomePage( + selectedView: CalendarView.week, + ), + mobileWidget: Scaffold( + floatingActionButton: FloatingActionButton( + child: Icon(Icons.add), + elevation: 8, + onPressed: () => context.pushRoute(CreateEventPage()), + ), + body: MultiDayViewWidget(), + ), + ); + } +} diff --git a/example/lib/widgets/multi_day_view_widget.dart b/example/lib/widgets/multi_day_view_widget.dart new file mode 100644 index 00000000..c1d7d53c --- /dev/null +++ b/example/lib/widgets/multi_day_view_widget.dart @@ -0,0 +1,48 @@ +import 'package:calendar_view/calendar_view.dart'; +import 'package:flutter/material.dart'; + +import '../pages/event_details_page.dart'; + +class MultiDayViewWidget extends StatelessWidget { + final GlobalKey? state; + final double? width; + + const MultiDayViewWidget({super.key, this.state, this.width}); + + @override + Widget build(BuildContext context) { + return MultiDayView( + key: state, + daysInView: 3, + width: width, + showLiveTimeLineInAllDays: true, + eventArranger: SideEventArranger(maxWidth: 30), + timeLineWidth: 65, + scrollPhysics: const BouncingScrollPhysics(), + liveTimeIndicatorSettings: LiveTimeIndicatorSettings( + color: Colors.redAccent, + onlyShowToday: true, + ), + onTimestampTap: (date) { + SnackBar snackBar = SnackBar( + content: Text("On tap: ${date.hour} Hr : ${date.minute} Min"), + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }, + onEventTap: (events, date) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => DetailsPage( + event: events.first, + date: date, + ), + ), + ); + }, + onEventLongTap: (events, date) { + SnackBar snackBar = SnackBar(content: Text("on LongTap")); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }, + ); + } +} diff --git a/lib/calendar_view.dart b/lib/calendar_view.dart index 80e038ea..40f6df95 100644 --- a/lib/calendar_view.dart +++ b/lib/calendar_view.dart @@ -18,3 +18,4 @@ export './src/month_view/month_view.dart'; export './src/style/header_style.dart'; export './src/typedefs.dart'; export './src/week_view/week_view.dart'; +export './src/multi_day_view/multi_day_view.dart'; diff --git a/lib/src/components/_internal_components.dart b/lib/src/components/_internal_components.dart index a13bd432..2afe54b2 100644 --- a/lib/src/components/_internal_components.dart +++ b/lib/src/components/_internal_components.dart @@ -41,17 +41,21 @@ class LiveTimeIndicator extends StatefulWidget { /// This field will be used to set end hour for day and week view final int endHour; + /// Flag to show only today's events. + final bool onlyShowToday; + /// Widget to display tile line according to current time. - const LiveTimeIndicator({ - Key? key, - required this.width, - required this.height, - required this.timeLineWidth, - required this.liveTimeIndicatorSettings, - required this.heightPerMinute, - required this.startHour, - this.endHour = Constants.hoursADay, - }) : super(key: key); + const LiveTimeIndicator( + {Key? key, + required this.width, + required this.height, + required this.timeLineWidth, + required this.liveTimeIndicatorSettings, + required this.heightPerMinute, + required this.startHour, + this.endHour = Constants.hoursADay, + this.onlyShowToday = false}) + : super(key: key); @override _LiveTimeIndicatorState createState() => _LiveTimeIndicatorState(); @@ -113,7 +117,9 @@ class _LiveTimeIndicatorState extends State { color: widget.liveTimeIndicatorSettings.color, height: widget.liveTimeIndicatorSettings.height, offset: Offset( - widget.timeLineWidth + widget.liveTimeIndicatorSettings.offset, + widget.onlyShowToday + ? 0 + : widget.timeLineWidth + widget.liveTimeIndicatorSettings.offset, (_currentTime.getTotalMinutes - startMinutes) * widget.heightPerMinute, ), diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index 7e6165d7..24eb7459 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -44,6 +44,24 @@ extension DateTimeExtensions on DateTime { 7) .ceil(); + /// Gets difference of multi-day between [date] and calling object. + int getMultiDayDifference( + {required DateTime startDate, + required DateTime endDate, + daysInView = 3}) { + final daysDifference = + startDate.withoutTime.difference(endDate.withoutTime).inDays.abs() + 1; + + return (daysDifference / daysInView).ceil(); + } + + /// Returns the list of [DateTime] for given [index] and [daysInView]. + List getMultiDateRangeList(DateTime startDate, int index, + {int daysInView = 3}) { + DateTime baseDate = startDate.add(Duration(days: index * daysInView)); + return List.generate(daysInView, (i) => baseDate.add(Duration(days: i))); + } + /// Returns The List of date of Current Week, all of the dates will be without /// time. /// Day will start from Monday to Sunday. @@ -89,6 +107,36 @@ extension DateTimeExtensions on DateTime { DateTime lastDayOfWeek({WeekDays start = WeekDays.monday}) => DateTime(year, month, day + (6 - (weekday - start.index - 1) % 7)); + DateTime firstDayOfMultiDay({ + required DateTime startDate, + int daysInView = 3, + }) { + final diffDays = startDate.withoutTime + .difference(DateTime.now().withoutTime) + .inDays + .abs(); + final offset = diffDays % daysInView; + return offset == 0 + ? startDate.withoutTime + : startDate.subtract(Duration(days: daysInView - offset)).withoutTime; + } + + /// Returns the last date of week containing the current date + DateTime lastDayOfMultiDay({ + required DateTime endDate, + int daysInView = 3, + }) { + final diffDays = endDate.withoutTime + .difference(DateTime.now().withoutTime) + .inDays + .abs() + + 1; + final offset = diffDays % daysInView; + return offset == 0 + ? endDate.withoutTime + : endDate.add(Duration(days: daysInView - offset)).withoutTime; + } + /// Returns list of all dates of [month]. /// All the dates are week based that means it will return array of size 42 /// which will contain 6 weeks that is the maximum number of weeks a month diff --git a/lib/src/modals.dart b/lib/src/modals.dart index dfd4af67..4e477859 100644 --- a/lib/src/modals.dart +++ b/lib/src/modals.dart @@ -62,6 +62,9 @@ class LiveTimeIndicatorSettings { /// Width of time backgroud view. final double timeBackgroundViewWidth; + /// Flag to show only today's events. + final bool onlyShowToday; + /// Settings for live time line const LiveTimeIndicatorSettings({ this.height = 1.0, @@ -73,6 +76,7 @@ class LiveTimeIndicatorSettings { this.showTimeBackgroundView = false, this.bulletRadius = 5.0, this.timeBackgroundViewWidth = 60.0, + this.onlyShowToday = false, }) : assert(height >= 0, "Height must be greater than or equal to 0."); factory LiveTimeIndicatorSettings.none() => LiveTimeIndicatorSettings( diff --git a/lib/src/multi_day_view/_internal_multi_day_view_page.dart b/lib/src/multi_day_view/_internal_multi_day_view_page.dart new file mode 100644 index 00000000..64886563 --- /dev/null +++ b/lib/src/multi_day_view/_internal_multi_day_view_page.dart @@ -0,0 +1,559 @@ +// Copyright (c) 2021 Simform Solutions. All rights reserved. +// Use of this source code is governed by a MIT-style license +// that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../components/_internal_components.dart'; +import '../components/event_scroll_notifier.dart'; +import '../components/week_view_components.dart'; +import '../enumerations.dart'; +import '../event_arrangers/event_arrangers.dart'; +import '../event_controller.dart'; +import '../modals.dart'; +import '../painters.dart'; +import '../typedefs.dart'; + +/// A single page for week view. +class InternalMultiDayViewPage extends StatefulWidget { + /// Width of the page. + final double width; + + /// Height of the page. + final double height; + + /// Dates to display on page. + final List dates; + + /// Builds tile for a single event. + final EventTileBuilder eventTileBuilder; + + /// A calendar controller that controls all the events and rebuilds widget + /// if event(s) are added or removed. + final EventController controller; + + /// A builder to build time line. + final DateWidgetBuilder timeLineBuilder; + + /// Settings for hour indicator lines. + final HourIndicatorSettings hourIndicatorSettings; + + /// Custom painter for hour line. + final CustomHourLinePainter hourLinePainter; + + /// Settings for half hour indicator lines. + final HourIndicatorSettings halfHourIndicatorSettings; + + /// Settings for quarter hour indicator lines. + final HourIndicatorSettings quarterHourIndicatorSettings; + + /// Flag to display live line. + final bool showLiveLine; + + /// Settings for live time indicator. + final LiveTimeIndicatorSettings liveTimeIndicatorSettings; + + /// Height occupied by one minute time span. + final double heightPerMinute; + + /// Width of timeline. + final double timeLineWidth; + + /// Offset of timeline. + final double timeLineOffset; + + /// Height occupied by one hour time span. + final double hourHeight; + + /// Arranger to arrange events. + final EventArranger eventArranger; + + /// Flag to display vertical line or not. + final bool showVerticalLine; + + /// Offset for vertical line offset. + final double verticalLineOffset; + + /// Builder for week day title. + final DateWidgetBuilder weekDayBuilder; + + /// Builder for week number. + final WeekNumberBuilder weekNumberBuilder; + + /// Builds custom PressDetector widget + final DetectorBuilder weekDetectorBuilder; + + /// Height of week title. + final double weekTitleHeight; + + /// Width of week title. + final double weekTitleWidth; + + /// Called when user taps on event tile. + final CellTapCallback? onTileTap; + + /// Called when user long press on event tile. + final CellTapCallback? onTileLongTap; + + /// Called when user double tap on any event tile. + final CellTapCallback? onTileDoubleTap; + + /// Defines which days should be displayed in one week. + /// + /// By default all the days will be visible. + /// Sequence will be monday to sunday. + final List weekDays; + + /// Called when user long press on calendar. + final DatePressCallback? onDateLongPress; + + /// Called when user taps on day view page. + /// + /// This callback will have a date parameter which + /// will provide the time span on which user has tapped. + /// + /// Ex, User Taps on Date page with date 11/01/2022 and time span is 1PM to 2PM. + /// then DateTime object will be DateTime(2022,01,11,1,0) + final DateTapCallback? onDateTap; + + /// Defines size of the slots that provides long press callback on area + /// where events are not there. + final MinuteSlotSize minuteSlotSize; + + final EventScrollConfiguration scrollConfiguration; + + /// Display full day events. + final FullDayEventBuilder fullDayEventBuilder; + + final ScrollController multiDayViewScrollController; + + /// First hour displayed in the layout + final int startHour; + + /// If true this will show week day at bottom position. + final bool showWeekDayAtBottom; + + /// Flag to display half hours + final bool showHalfHours; + + /// Flag to display quarter hours + final bool showQuarterHours; + + /// Display workday bottom line + final bool showWeekDayBottomLine; + + /// Emulate vertical line offset from hour line starts. + final double emulateVerticalOffsetBy; + + /// This field will be used to set end hour for week view + final int endHour; + + /// Title of the full day events row + final String fullDayHeaderTitle; + + /// Defines full day events header text config + final FullDayHeaderTextConfig fullDayHeaderTextConfig; + + /// Scroll listener to set every page's last offset + final void Function(ScrollController) scrollListener; + + /// Last scroll offset of week view page. + final double lastScrollOffset; + + /// Flag to keep scrollOffset of pages on page change + final bool keepScrollOffset; + + /// Use this field to disable the calendar scrolling + final ScrollPhysics? scrollPhysics; + + /// This method will be called when user taps on timestamp in timeline. + final TimestampCallback? onTimestampTap; + + /// A single page for week view. + const InternalMultiDayViewPage( + {Key? key, + required this.showVerticalLine, + required this.weekTitleHeight, + required this.weekDayBuilder, + required this.weekNumberBuilder, + required this.width, + required this.dates, + required this.eventTileBuilder, + required this.controller, + required this.timeLineBuilder, + required this.hourIndicatorSettings, + required this.hourLinePainter, + required this.halfHourIndicatorSettings, + required this.quarterHourIndicatorSettings, + required this.showLiveLine, + required this.liveTimeIndicatorSettings, + required this.heightPerMinute, + required this.timeLineWidth, + required this.timeLineOffset, + required this.height, + required this.hourHeight, + required this.eventArranger, + required this.verticalLineOffset, + required this.weekTitleWidth, + required this.onTileTap, + required this.onTileLongTap, + required this.onDateLongPress, + required this.onDateTap, + required this.weekDays, + required this.minuteSlotSize, + required this.scrollConfiguration, + required this.startHour, + required this.fullDayEventBuilder, + required this.weekDetectorBuilder, + required this.showWeekDayAtBottom, + required this.showHalfHours, + required this.showQuarterHours, + required this.emulateVerticalOffsetBy, + required this.onTileDoubleTap, + required this.endHour, + required this.onTimestampTap, + this.fullDayHeaderTitle = '', + required this.fullDayHeaderTextConfig, + required this.scrollPhysics, + required this.scrollListener, + required this.multiDayViewScrollController, + this.lastScrollOffset = 0.0, + this.keepScrollOffset = false, + this.showWeekDayBottomLine = true}) + : super(key: key); + + @override + _InternalMultiDayViewPageState createState() => + _InternalMultiDayViewPageState(); +} + +class _InternalMultiDayViewPageState + extends State> { + late ScrollController scrollController; + + @override + void initState() { + super.initState(); + scrollController = ScrollController( + initialScrollOffset: widget.lastScrollOffset, + ); + scrollController.addListener(_scrollControllerListener); + } + + @override + void dispose() { + scrollController + ..removeListener(_scrollControllerListener) + ..dispose(); + super.dispose(); + } + + void _scrollControllerListener() { + widget.scrollListener(scrollController); + } + + @override + Widget build(BuildContext context) { + final filteredDates = _filteredDate(); + return Container( + height: widget.height + widget.weekTitleHeight, + width: widget.width, + child: Column( + verticalDirection: widget.showWeekDayAtBottom + ? VerticalDirection.up + : VerticalDirection.down, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Color(0x0C000000), + offset: Offset(0, 2), + blurRadius: 12, + spreadRadius: 0, + ), + ], + ), + child: SizedBox( + width: widget.width, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: widget.weekTitleHeight, + width: widget.timeLineWidth + + widget.hourIndicatorSettings.offset, + child: widget.weekNumberBuilder.call(filteredDates[0]), + ), + ...List.generate( + filteredDates.length, + (index) => SizedBox( + height: widget.weekTitleHeight, + width: widget.weekTitleWidth, + child: widget.weekDayBuilder( + filteredDates[index], + ), + ), + ) + ], + ), + ), + ), + if (widget.showWeekDayBottomLine) + Divider( + thickness: 1, + height: 1, + ), + SizedBox( + width: widget.width, + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: widget.hourIndicatorSettings.color, + width: 2, + ), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: widget.timeLineWidth + + widget.hourIndicatorSettings.offset, + child: widget.fullDayHeaderTitle.isNotEmpty + ? Padding( + padding: const EdgeInsets.symmetric( + vertical: 2, + horizontal: 1, + ), + child: Text( + widget.fullDayHeaderTitle, + textAlign: + widget.fullDayHeaderTextConfig.textAlign, + maxLines: widget.fullDayHeaderTextConfig.maxLines, + overflow: + widget.fullDayHeaderTextConfig.textOverflow, + ), + ) + : SizedBox.shrink(), + ), + ...List.generate( + filteredDates.length, + (index) { + final fullDayEventList = widget.controller + .getFullDayEvent(filteredDates[index]); + return Container( + width: widget.weekTitleWidth, + child: fullDayEventList.isEmpty + ? null + : widget.fullDayEventBuilder.call( + fullDayEventList, + widget.dates[index], + ), + ); + }, + ) + ], + ), + ), + ), + Expanded( + child: SingleChildScrollView( + controller: widget.keepScrollOffset + ? scrollController + : widget.multiDayViewScrollController, + physics: widget.scrollPhysics, + child: SizedBox( + height: widget.height, + width: widget.width, + child: Stack( + children: [ + CustomPaint( + size: Size(widget.width, widget.height), + painter: widget.hourLinePainter( + widget.hourIndicatorSettings.color, + widget.hourIndicatorSettings.height, + widget.timeLineWidth + + widget.hourIndicatorSettings.offset, + widget.heightPerMinute, + widget.showVerticalLine, + widget.verticalLineOffset, + widget.hourIndicatorSettings.lineStyle, + widget.hourIndicatorSettings.dashWidth, + widget.hourIndicatorSettings.dashSpaceWidth, + widget.emulateVerticalOffsetBy, + widget.startHour, + widget.endHour, + ), + ), + if (widget.showHalfHours) + CustomPaint( + size: Size(widget.width, widget.height), + painter: HalfHourLinePainter( + lineColor: widget.halfHourIndicatorSettings.color, + lineHeight: widget.halfHourIndicatorSettings.height, + offset: widget.timeLineWidth + + widget.halfHourIndicatorSettings.offset, + minuteHeight: widget.heightPerMinute, + lineStyle: widget.halfHourIndicatorSettings.lineStyle, + dashWidth: widget.halfHourIndicatorSettings.dashWidth, + dashSpaceWidth: + widget.halfHourIndicatorSettings.dashSpaceWidth, + startHour: widget.halfHourIndicatorSettings.startHour, + endHour: widget.endHour, + ), + ), + if (widget.showQuarterHours) + CustomPaint( + size: Size(widget.width, widget.height), + painter: QuarterHourLinePainter( + lineColor: widget.quarterHourIndicatorSettings.color, + lineHeight: + widget.quarterHourIndicatorSettings.height, + offset: widget.timeLineWidth + + widget.quarterHourIndicatorSettings.offset, + minuteHeight: widget.heightPerMinute, + lineStyle: + widget.quarterHourIndicatorSettings.lineStyle, + dashWidth: + widget.quarterHourIndicatorSettings.dashWidth, + dashSpaceWidth: widget + .quarterHourIndicatorSettings.dashSpaceWidth, + ), + ), + Align( + alignment: Alignment.centerRight, + child: SizedBox( + width: widget.weekTitleWidth * filteredDates.length, + height: widget.height, + child: Row( + children: [ + ...List.generate( + filteredDates.length, + (index) => Container( + decoration: widget.showVerticalLine + ? BoxDecoration( + border: Border( + right: BorderSide( + color: widget + .hourIndicatorSettings.color, + width: widget + .hourIndicatorSettings.height, + ), + ), + ) + : null, + height: widget.height, + width: widget.weekTitleWidth, + child: Stack( + children: [ + widget.weekDetectorBuilder( + width: widget.weekTitleWidth, + height: widget.height, + heightPerMinute: widget.heightPerMinute, + date: widget.dates[index], + minuteSlotSize: widget.minuteSlotSize, + ), + EventGenerator( + height: widget.height, + date: filteredDates[index], + onTileTap: widget.onTileTap, + onTileLongTap: widget.onTileLongTap, + onTileDoubleTap: widget.onTileDoubleTap, + width: widget.weekTitleWidth, + eventArranger: widget.eventArranger, + eventTileBuilder: widget.eventTileBuilder, + scrollNotifier: + widget.scrollConfiguration, + startHour: widget.startHour, + events: widget.controller.getEventsOnDay( + filteredDates[index], + includeFullDayEvents: false, + ), + heightPerMinute: widget.heightPerMinute, + endHour: widget.endHour, + ), + if (widget.showLiveLine && + widget.liveTimeIndicatorSettings + .height > + 0 && + widget.liveTimeIndicatorSettings + .onlyShowToday) + if (DateUtils.isSameDay( + widget.dates[index], DateTime.now())) + LiveTimeIndicator( + liveTimeIndicatorSettings: + widget.liveTimeIndicatorSettings, + width: widget.width, + height: widget.height, + heightPerMinute: + widget.heightPerMinute, + timeLineWidth: widget.timeLineWidth, + startHour: widget.startHour, + endHour: widget.endHour, + onlyShowToday: widget + .liveTimeIndicatorSettings + .onlyShowToday, + ), + ], + ), + ), + ) + ], + ), + ), + ), + TimeLine( + timeLineWidth: widget.timeLineWidth, + hourHeight: widget.hourHeight, + height: widget.height, + timeLineOffset: widget.timeLineOffset, + timeLineBuilder: widget.timeLineBuilder, + startHour: widget.startHour, + showHalfHours: widget.showHalfHours, + showQuarterHours: widget.showQuarterHours, + liveTimeIndicatorSettings: + widget.liveTimeIndicatorSettings, + endHour: widget.endHour, + onTimestampTap: widget.onTimestampTap, + ), + if (widget.showLiveLine && + widget.liveTimeIndicatorSettings.height > 0 && + !widget.liveTimeIndicatorSettings.onlyShowToday) + LiveTimeIndicator( + liveTimeIndicatorSettings: + widget.liveTimeIndicatorSettings, + width: widget.width, + height: widget.height, + heightPerMinute: widget.heightPerMinute, + timeLineWidth: widget.timeLineWidth, + startHour: widget.startHour, + endHour: widget.endHour, + ), + ], + ), + ), + ), + ), + ], + ), + ); + } + + List _filteredDate() { + final output = []; + + final weekDays = widget.weekDays.toList(); + + for (final date in widget.dates) { + if (weekDays.any((weekDay) => weekDay.index + 1 == date.weekday)) { + output.add(date); + } + } + + return output; + } +} diff --git a/lib/src/multi_day_view/multi_day_view.dart b/lib/src/multi_day_view/multi_day_view.dart new file mode 100644 index 00000000..d7e5c4bd --- /dev/null +++ b/lib/src/multi_day_view/multi_day_view.dart @@ -0,0 +1,1079 @@ +// Copyright (c) 2021 Simform Solutions. All rights reserved. +// Use of this source code is governed by a MIT-style license +// that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../calendar_constants.dart'; +import '../calendar_controller_provider.dart'; +import '../calendar_event_data.dart'; +import '../components/components.dart'; +import '../constants.dart'; +import '../enumerations.dart'; +import '../event_arrangers/event_arrangers.dart'; +import '../event_controller.dart'; +import '../extensions.dart'; +import '../modals.dart'; +import '../painters.dart'; +import '../style/header_style.dart'; +import '../typedefs.dart'; +import '_internal_multi_day_view_page.dart'; + +/// [Widget] to display week view. +class MultiDayView extends StatefulWidget { + /// Builder to build tile for events. + final EventTileBuilder? eventTileBuilder; + + /// Builder for timeline. + final DateWidgetBuilder? timeLineBuilder; + + /// Header builder for week page header. + /// + /// If there are some configurations that is not directly available + /// in [MultiDayView], override this to create your custom header or reuse, + /// [CalendarPageHeader] | [DayPageHeader] | [MonthPageHeader] | + /// [WeekPageHeader] widgets provided by this package with your custom + /// configurations. + /// + final WeekPageHeaderBuilder? weekPageHeaderBuilder; + + /// Builds custom PressDetector widget + /// + /// If null, internal PressDetector will be used to handle onDateLongPress() + /// + final DetectorBuilder? weekDetectorBuilder; + + /// This function will generate dateString int the calendar header. + /// Useful for I18n + final StringProvider? headerStringBuilder; + + /// This function will generate the TimeString in the timeline. + /// Useful for I18n + final StringProvider? timeLineStringBuilder; + + /// This function will generate WeekDayString in the weekday. + /// Useful for I18n + final String Function(int)? weekDayStringBuilder; + + /// This function will generate WeekDayDateString in the weekday. + /// Useful for I18n + final String Function(int)? weekDayDateStringBuilder; + + /// Arrange events. + final EventArranger? eventArranger; + + /// Called whenever user changes week. + final CalendarPageChangeCallBack? onPageChange; + + /// Minimum day to display in week view. + /// + /// In calendar first date of the week that contains this data will be + /// minimum date. + /// + /// ex, If minDay is 16th March, 2022 then week containing this date will have + /// dates from 14th to 20th (Monday to Sunday). adn 14th date will + /// be the actual minimum date. + final DateTime? minDay; + + /// Maximum day to display in week view. + /// + /// In calendar last date of the week that contains this data will be + /// maximum date. + /// + /// ex, If maxDay is 16th March, 2022 then week containing this date will have + /// dates from 14th to 20th (Monday to Sunday). adn 20th date will + /// be the actual maximum date. + final DateTime? maxDay; + + /// Initial week to display in week view. + final DateTime? initialDay; + + /// Settings for hour indicator settings. + final HourIndicatorSettings? hourIndicatorSettings; + + /// A funtion that returns a [CustomPainter]. + /// + /// Use this if you want to paint custom hour lines. + final CustomHourLinePainter? hourLinePainter; + + /// Settings for half hour indicator settings. + final HourIndicatorSettings? halfHourIndicatorSettings; + + /// Settings for quarter hour indicator settings. + final HourIndicatorSettings? quarterHourIndicatorSettings; + + /// Settings for live time indicator settings. + final LiveTimeIndicatorSettings? liveTimeIndicatorSettings; + + /// duration for page transition while changing the week. + final Duration pageTransitionDuration; + + /// Transition curve for transition. + final Curve pageTransitionCurve; + + /// Controller for Week view thia will refresh view when user adds or removes + /// event from controller. + final EventController? controller; + + /// Defines height occupied by one minute of time span. This parameter will + /// be used to calculate total height of Week view. + final double heightPerMinute; + + /// Width of time line. + final double? timeLineWidth; + + /// Flag to show live time indicator in all day or only [initialDay] + final bool showLiveTimeLineInAllDays; + + /// Offset of time line + final double timeLineOffset; + + /// Width of week view. If null provided device width will be considered. + final double? width; + + /// If true this will display vertical lines between each day. + final bool showVerticalLines; + + /// Height of week day title, + final double weekTitleHeight; + + /// Builder to build week day. + final DateWidgetBuilder? weekDayBuilder; + + /// Builder to build week number. + final WeekNumberBuilder? weekNumberBuilder; + + /// Background color of week view page. + final Color backgroundColor; + + /// Scroll offset of week view page. + final double scrollOffset; + + /// This method will be called when user taps on timestamp in timeline. + final TimestampCallback? onTimestampTap; + + /// Called when user taps on event tile. + final CellTapCallback? onEventTap; + + /// Called when user long press on event tile. + final CellTapCallback? onEventLongTap; + + /// Called when user double taps on any event tile. + final CellTapCallback? onEventDoubleTap; + + /// Show weekends or not + /// + /// Default value is true. + /// + /// If it is false week view will remove weekends from week + /// even if weekends are added in [weekDays]. + /// + /// ex, if [showWeekends] is false and [weekDays] are monday, tuesday, + /// saturday and sunday, only monday and tuesday will be visible in week view. + // final bool showWeekends; + + /// Defines which days should be displayed in one week. + /// + /// By default all the days will be visible. + /// Sequence will be monday to sunday. + /// + /// Duplicate values will be removed from list. + /// + /// ex, if there are two mondays in list it will display only one. + // final List weekDays; + + /// This method will be called when user long press on calendar. + final DatePressCallback? onDateLongPress; + + /// Called when user taps on day view page. + /// + /// This callback will have a date parameter which + /// will provide the time span on which user has tapped. + /// + /// Ex, User Taps on Date page with date 11/01/2022 and time span is 1PM to 2PM. + /// then DateTime object will be DateTime(2022,01,11,1,0) + final DateTapCallback? onDateTap; + + /// Defines the day from which the week starts. + /// + /// Default value is [WeekDays.monday]. + // final WeekDays startDay; + + /// Defines size of the slots that provides long press callback on area + /// where events are not there. + final MinuteSlotSize minuteSlotSize; + + /// Style for MultiDayView header. + final HeaderStyle headerStyle; + + /// Option for SafeArea. + final SafeAreaOption safeAreaOption; + + /// Display full day event builder. + final FullDayEventBuilder? fullDayEventBuilder; + + /// First hour displayed in the layout, goes from 0 to 24 + final int startHour; + + /// This field will be used to set end hour for week view + final int endHour; + + ///Show half hour indicator + final bool showHalfHours; + + ///Show quarter hour indicator + final bool showQuarterHours; + + ///Emulates offset of vertical line from hour line starts. + final double emulateVerticalOffsetBy; + + /// Callback for the Header title + final HeaderTitleCallback? onHeaderTitleTap; + + /// If true this will show week day at bottom position. + final bool showWeekDayAtBottom; + + /// Use this field to disable the calendar scrolling + final ScrollPhysics? scrollPhysics; + + /// Defines scroll physics for a page of a week view. + /// + /// This can be used to disable the horizontal scroll of a page. + final ScrollPhysics? pageViewPhysics; + + /// Title of the full day events row + final String fullDayHeaderTitle; + + /// Defines full day events header text config + final FullDayHeaderTextConfig? fullDayHeaderTextConfig; + + /// Flag to keep scrollOffset of pages on page change + final bool keepScrollOffset; + + /// Number of days to display in the view, Default to 3 days. + final int daysInView; + + /// Display workday bottom line + final bool showWeekDayBottomLine; + + /// Main widget for week view. + const MultiDayView({ + Key? key, + this.controller, + this.eventTileBuilder, + this.pageTransitionDuration = const Duration(milliseconds: 300), + this.pageTransitionCurve = Curves.ease, + this.heightPerMinute = 1, + this.timeLineOffset = 0, + this.showLiveTimeLineInAllDays = false, + this.showVerticalLines = true, + this.width, + this.minDay, + this.maxDay, + this.initialDay, + this.hourIndicatorSettings, + this.hourLinePainter, + this.halfHourIndicatorSettings, + this.quarterHourIndicatorSettings, + this.timeLineBuilder, + this.timeLineWidth, + this.liveTimeIndicatorSettings, + this.onPageChange, + this.weekPageHeaderBuilder, + this.eventArranger, + this.weekTitleHeight = 50, + this.weekDayBuilder, + this.weekNumberBuilder, + this.backgroundColor = Colors.white, + this.scrollPhysics, + this.scrollOffset = 0.0, + this.onEventTap, + this.onEventLongTap, + this.onDateLongPress, + this.onDateTap, + // this.weekDays = WeekDays.values, + // this.showWeekends = true, + // this.startDay = WeekDays.monday, + this.minuteSlotSize = MinuteSlotSize.minutes60, + this.weekDetectorBuilder, + this.headerStringBuilder, + this.timeLineStringBuilder, + this.weekDayStringBuilder, + this.weekDayDateStringBuilder, + this.headerStyle = const HeaderStyle(), + this.safeAreaOption = const SafeAreaOption(), + this.fullDayEventBuilder, + this.startHour = 0, + this.onHeaderTitleTap, + this.showHalfHours = false, + this.showQuarterHours = false, + this.emulateVerticalOffsetBy = 0, + this.showWeekDayAtBottom = false, + this.pageViewPhysics, + this.onEventDoubleTap, + this.endHour = Constants.hoursADay, + this.fullDayHeaderTitle = '', + this.fullDayHeaderTextConfig, + this.keepScrollOffset = false, + this.onTimestampTap, + this.daysInView = 3, + this.showWeekDayBottomLine = true, + }) : assert(!(onHeaderTitleTap != null && weekPageHeaderBuilder != null), + "can't use [onHeaderTitleTap] & [weekPageHeaderBuilder] simultaneously"), + assert((timeLineOffset) >= 0, + "timeLineOffset must be greater than or equal to 0"), + assert(width == null || width > 0, + "Calendar width must be greater than 0."), + assert(timeLineWidth == null || timeLineWidth > 0, + "Time line width must be greater than 0."), + assert( + heightPerMinute > 0, "Height per minute must be greater than 0."), + assert( + weekDetectorBuilder == null || onDateLongPress == null, + """If you use [weekPressDetectorBuilder] + do not provide [onDateLongPress]""", + ), + assert( + startHour <= 0 || startHour != endHour, + "startHour must be greater than 0 or startHour should not equal to endHour", + ), + assert( + endHour <= Constants.hoursADay || endHour < startHour, + "End hour must be less than 24 or startHour must be less than endHour", + ), + super(key: key); + + @override + MultiDayViewState createState() => MultiDayViewState(); +} + +class MultiDayViewState extends State> { + late double _width; + late double _height; + late double _timeLineWidth; + late double _hourHeight; + late double _lastScrollOffset; + late DateTime _currentStartDate; + late DateTime _currentEndDate; + late DateTime _maxDate; + late DateTime _minDate; + late DateTime _currentWeek; + late int _totalWeeks; + late int _currentIndex; + late String _fullDayHeaderTitle; + + late EventArranger _eventArranger; + + late HourIndicatorSettings _hourIndicatorSettings; + late CustomHourLinePainter _hourLinePainter; + + late HourIndicatorSettings _halfHourIndicatorSettings; + late LiveTimeIndicatorSettings _liveTimeIndicatorSettings; + late HourIndicatorSettings _quarterHourIndicatorSettings; + + late PageController _pageController; + + late DateWidgetBuilder _timeLineBuilder; + late EventTileBuilder _eventTileBuilder; + late WeekPageHeaderBuilder _weekHeaderBuilder; + late DateWidgetBuilder _weekDayBuilder; + late WeekNumberBuilder _weekNumberBuilder; + late FullDayEventBuilder _fullDayEventBuilder; + late DetectorBuilder _weekDetectorBuilder; + late FullDayHeaderTextConfig _fullDayHeaderTextConfig; + + late double _weekTitleWidth; + late int _totalDaysInWeek; + + late VoidCallback _reloadCallback; + + EventController? _controller; + + late ScrollController _scrollController; + + ScrollController get scrollController => _scrollController; + + late List _weekDays; + + late int _startHour; + late int _endHour; + + final _scrollConfiguration = EventScrollConfiguration(); + + @override + void initState() { + super.initState(); + _lastScrollOffset = widget.scrollOffset; + + _scrollController = + ScrollController(initialScrollOffset: widget.scrollOffset); + + _startHour = widget.startHour; + _endHour = widget.endHour; + + _reloadCallback = _reload; + + _setWeekDays(); + _setDateRange(); + + _currentWeek = (widget.initialDay ?? DateTime.now()).withoutTime; + + _regulateCurrentDate(); + + _calculateHeights(); + + _pageController = PageController(initialPage: _currentIndex); + _eventArranger = widget.eventArranger ?? SideEventArranger(); + + _assignBuilders(); + _fullDayHeaderTitle = widget.fullDayHeaderTitle; + _fullDayHeaderTextConfig = + widget.fullDayHeaderTextConfig ?? FullDayHeaderTextConfig(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final newController = widget.controller ?? + CalendarControllerProvider.of(context).controller; + + if (_controller != newController) { + _controller = newController; + + _controller! + // Removes existing callback. + ..removeListener(_reloadCallback) + + // Reloads the view if there is any change in controller or + // user adds new events. + ..addListener(_reloadCallback); + } + } + + @override + void didUpdateWidget(MultiDayView oldWidget) { + super.didUpdateWidget(oldWidget); + // Update controller. + final newController = widget.controller ?? + CalendarControllerProvider.of(context).controller; + + if (newController != _controller) { + _controller?.removeListener(_reloadCallback); + _controller = newController; + _controller?.addListener(_reloadCallback); + } + + _setWeekDays(); + + // Update date range. + if (widget.minDay != oldWidget.minDay || + widget.maxDay != oldWidget.maxDay) { + _setDateRange(); + _regulateCurrentDate(); + // updateRange(); + + _pageController.jumpToPage(_currentIndex); + } + + _eventArranger = widget.eventArranger ?? SideEventArranger(); + _startHour = widget.startHour; + _endHour = widget.endHour; + + // Update heights. + _calculateHeights(); + + // Update builders and callbacks + _assignBuilders(); + + if (widget.scrollOffset != oldWidget.scrollOffset) { + _lastScrollOffset = widget.scrollOffset; + _scrollController.jumpTo(widget.scrollOffset); + } + } + + // void updateRange() { + // // Initially current start date might not had been updated + // _minDate = DateTime( + // _currentStartDate.year, + // _currentStartDate.month, + // _currentStartDate.day - _currentIndex * widget.daysInView, + // ); + + // // TODO(Shubham): Update max date + // debugPrint('Old max date: ${_maxDate}'); + // } + + @override + void dispose() { + _controller?.removeListener(_reloadCallback); + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SafeAreaWrapper( + option: widget.safeAreaOption, + child: LayoutBuilder(builder: (context, constraint) { + _width = widget.width ?? constraint.maxWidth; + _updateViewDimensions(); + return SizedBox( + width: _width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _weekHeaderBuilder( + _currentStartDate, + _currentEndDate, + ), + Expanded( + child: DecoratedBox( + decoration: BoxDecoration(color: widget.backgroundColor), + child: SizedBox( + height: _height, + width: _width, + child: PageView.builder( + itemCount: _totalWeeks, + controller: _pageController, + physics: widget.pageViewPhysics, + onPageChanged: _onPageChange, + itemBuilder: (_, index) { + // final dates = DateTime(_minDate.year, _minDate.month, + // _minDate.day + (index * DateTime.daysPerWeek)) + // .datesOfWeek( + // start: widget.startDay, + // showWeekEnds: widget.showWeekends, + // ); + + final dates = _minDate.getMultiDateRangeList( + _minDate.withoutTime, index, + daysInView: widget.daysInView); + + return ValueListenableBuilder( + valueListenable: _scrollConfiguration, + builder: (_, __, ___) => InternalMultiDayViewPage( + key: ValueKey( + _hourHeight.toString() + dates[0].toString()), + height: _height, + width: _width, + weekTitleWidth: _weekTitleWidth, + weekTitleHeight: widget.weekTitleHeight, + weekDayBuilder: _weekDayBuilder, + weekNumberBuilder: _weekNumberBuilder, + weekDetectorBuilder: _weekDetectorBuilder, + liveTimeIndicatorSettings: + _liveTimeIndicatorSettings, + timeLineBuilder: _timeLineBuilder, + onTimestampTap: widget.onTimestampTap, + onTileTap: widget.onEventTap, + onTileLongTap: widget.onEventLongTap, + onDateLongPress: widget.onDateLongPress, + onDateTap: widget.onDateTap, + onTileDoubleTap: widget.onEventDoubleTap, + eventTileBuilder: _eventTileBuilder, + heightPerMinute: widget.heightPerMinute, + hourIndicatorSettings: _hourIndicatorSettings, + hourLinePainter: _hourLinePainter, + halfHourIndicatorSettings: + _halfHourIndicatorSettings, + quarterHourIndicatorSettings: + _quarterHourIndicatorSettings, + dates: dates, + showLiveLine: widget.showLiveTimeLineInAllDays || + _showLiveTimeIndicator(dates), + timeLineOffset: widget.timeLineOffset, + timeLineWidth: _timeLineWidth, + verticalLineOffset: 0, + showVerticalLine: widget.showVerticalLines, + controller: controller, + hourHeight: _hourHeight, + multiDayViewScrollController: _scrollController, + eventArranger: _eventArranger, + showWeekDayBottomLine: widget.showWeekDayBottomLine, + weekDays: _weekDays, + minuteSlotSize: widget.minuteSlotSize, + scrollConfiguration: _scrollConfiguration, + fullDayEventBuilder: _fullDayEventBuilder, + startHour: _startHour, + showHalfHours: widget.showHalfHours, + showQuarterHours: widget.showQuarterHours, + emulateVerticalOffsetBy: + widget.emulateVerticalOffsetBy, + showWeekDayAtBottom: widget.showWeekDayAtBottom, + endHour: _endHour, + fullDayHeaderTitle: _fullDayHeaderTitle, + fullDayHeaderTextConfig: _fullDayHeaderTextConfig, + lastScrollOffset: _lastScrollOffset, + scrollPhysics: widget.scrollPhysics, + scrollListener: _scrollPageListener, + keepScrollOffset: widget.keepScrollOffset, + ), + ); + }, + ), + ), + ), + ), + ], + ), + ); + }), + ); + } + + /// Returns [EventController] associated with this Widget. + /// + /// This will throw [AssertionError] if controller is called before its + /// initialization is complete. + EventController get controller { + if (_controller == null) { + throw "EventController is not initialized yet."; + } + + return _controller!; + } + + /// Reloads page. + void _reload() { + if (mounted) { + setState(() {}); + } + } + + void _setWeekDays() { + _weekDays = WeekDays.values.toSet().toList(); + + // if (!widget.showWeekends) { + // _weekDays + // ..remove(WeekDays.saturday) + // ..remove(WeekDays.sunday); + // } + + assert( + _weekDays.isNotEmpty, + "weekDays can not be empty.\n" + "Make sure you are providing weekdays in initialization of " + "MultiDayView. or showWeekends is true if you are providing only " + "saturday or sunday in weekDays."); + _totalDaysInWeek = widget.daysInView; + } + + void _updateViewDimensions() { + _timeLineWidth = widget.timeLineWidth ?? _width * 0.13; + + _liveTimeIndicatorSettings = widget.liveTimeIndicatorSettings ?? + LiveTimeIndicatorSettings( + color: Constants.defaultLiveTimeIndicatorColor, + height: widget.heightPerMinute, + ); + + assert(_liveTimeIndicatorSettings.height < _hourHeight, + "liveTimeIndicator height must be less than minuteHeight * 60"); + + _hourIndicatorSettings = widget.hourIndicatorSettings ?? + HourIndicatorSettings( + height: widget.heightPerMinute, + color: Constants.defaultBorderColor, + offset: 5, + ); + + assert(_hourIndicatorSettings.height < _hourHeight, + "hourIndicator height must be less than minuteHeight * 60"); + + _weekTitleWidth = + (_width - _timeLineWidth - _hourIndicatorSettings.offset) / + _totalDaysInWeek; + + _halfHourIndicatorSettings = widget.halfHourIndicatorSettings ?? + HourIndicatorSettings( + height: widget.heightPerMinute, + color: Constants.defaultBorderColor, + offset: 5, + ); + + assert(_halfHourIndicatorSettings.height < _hourHeight, + "halfHourIndicator height must be less than minuteHeight * 60"); + + _quarterHourIndicatorSettings = widget.quarterHourIndicatorSettings ?? + HourIndicatorSettings( + color: Constants.defaultBorderColor, + ); + + assert(_quarterHourIndicatorSettings.height < _hourHeight, + "quarterHourIndicator height must be less than minuteHeight * 60"); + } + + void _calculateHeights() { + _hourHeight = widget.heightPerMinute * 60; + _height = _hourHeight * (_endHour - _startHour); + } + + void _assignBuilders() { + _timeLineBuilder = widget.timeLineBuilder ?? _defaultTimeLineBuilder; + _eventTileBuilder = widget.eventTileBuilder ?? _defaultEventTileBuilder; + _weekHeaderBuilder = + widget.weekPageHeaderBuilder ?? _defaultWeekPageHeaderBuilder; + _weekDayBuilder = widget.weekDayBuilder ?? _defaultWeekDayBuilder; + _weekDetectorBuilder = + widget.weekDetectorBuilder ?? _defaultPressDetectorBuilder; + _weekNumberBuilder = widget.weekNumberBuilder ?? _defaultWeekNumberBuilder; + _fullDayEventBuilder = + widget.fullDayEventBuilder ?? _defaultFullDayEventBuilder; + _hourLinePainter = widget.hourLinePainter ?? _defaultHourLinePainter; + } + + Widget _defaultFullDayEventBuilder( + List> events, DateTime dateTime) { + return FullDayEventView( + events: events, + boxConstraints: BoxConstraints(maxHeight: 65), + date: dateTime, + onEventTap: widget.onEventTap, + onEventDoubleTap: widget.onEventDoubleTap, + onEventLongPress: widget.onEventLongTap, + ); + } + + /// Sets the current date of this month. + /// + /// This method is used in initState and onUpdateWidget methods to + /// regulate current date in Month view. + /// + /// If maximum and minimum dates are change then first call _setDateRange + /// and then _regulateCurrentDate method. + /// + void _regulateCurrentDate() { + if (_currentWeek.isBefore(_minDate)) { + _currentWeek = _minDate; + } else if (_currentWeek.isAfter(_maxDate)) { + _currentWeek = _maxDate; + } + _currentStartDate = _currentWeek; + _currentEndDate = _currentWeek.lastDayOfMultiDay( + endDate: _currentWeek, daysInView: widget.daysInView); + _currentIndex = _minDate.getMultiDayDifference( + startDate: _minDate.withoutTime, + endDate: _currentWeek, + daysInView: widget.daysInView, + ) - + 1; + } + + /// Sets the minimum and maximum dates for current view. + void _setDateRange() { + _minDate = (widget.minDay ?? CalendarConstants.epochDate) + .firstDayOfMultiDay( + startDate: (widget.minDay ?? CalendarConstants.epochDate), + daysInView: widget.daysInView) + .withoutTime; + + _maxDate = (widget.maxDay ?? CalendarConstants.maxDate) + .lastDayOfMultiDay( + endDate: (widget.maxDay ?? CalendarConstants.maxDate), + daysInView: widget.daysInView) + .withoutTime; + + assert( + _minDate.isBefore(_maxDate), + "Minimum date must be less than maximum date.\n" + "Provided minimum date: $_minDate, maximum date: $_maxDate", + ); + _totalWeeks = _minDate.getMultiDayDifference( + startDate: _minDate, endDate: _maxDate, daysInView: widget.daysInView); + } + + /// Default press detector builder. This builder will be used if + /// [widget.weekDetectorBuilder] is null. + /// + Widget _defaultPressDetectorBuilder({ + required DateTime date, + required double height, + required double width, + required double heightPerMinute, + required MinuteSlotSize minuteSlotSize, + }) => + DefaultPressDetector( + date: date, + height: height, + width: width, + heightPerMinute: heightPerMinute, + minuteSlotSize: minuteSlotSize, + onDateTap: widget.onDateTap, + onDateLongPress: widget.onDateLongPress, + startHour: _startHour, + ); + + /// Default builder for week line. + Widget _defaultWeekDayBuilder(DateTime date) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(widget.weekDayStringBuilder?.call(date.weekday - 1) ?? + Constants.weekTitles[date.weekday - 1]), + Text(widget.weekDayDateStringBuilder?.call(date.day) ?? + date.day.toString()), + ], + ), + ); + } + + /// Default builder for week number. + Widget _defaultWeekNumberBuilder(DateTime date) { + final daysToAdd = DateTime.thursday - date.weekday; + final thursday = daysToAdd > 0 + ? date.add(Duration(days: daysToAdd)) + : date.subtract(Duration(days: daysToAdd.abs())); + final weekNumber = + (date.difference(DateTime(thursday.year)).inDays / 7).floor() + 1; + return Center( + child: Text("$weekNumber"), + ); + } + + /// Default timeline builder this builder will be used if + /// [widget.eventTileBuilder] is null + /// + Widget _defaultTimeLineBuilder(DateTime date) => DefaultTimeLineMark( + date: date, + timeStringBuilder: widget.timeLineStringBuilder, + ); + + /// Default timeline builder. This builder will be used if + /// [widget.eventTileBuilder] is null + Widget _defaultEventTileBuilder( + DateTime date, + List> events, + Rect boundary, + DateTime startDuration, + DateTime endDuration, + ) => + DefaultEventTile( + date: date, + events: events, + boundary: boundary, + startDuration: startDuration, + endDuration: endDuration, + ); + + /// Default view header builder. This builder will be used if + /// [widget.dayTitleBuilder] is null. + Widget _defaultWeekPageHeaderBuilder( + DateTime startDate, + DateTime endDate, + ) { + return WeekPageHeader( + startDate: _currentStartDate, + endDate: _currentEndDate, + onNextDay: nextPage, + showNextIcon: endDate != _maxDate, + onPreviousDay: previousPage, + showPreviousIcon: startDate != _minDate, + onTitleTapped: () async { + if (widget.onHeaderTitleTap != null) { + widget.onHeaderTitleTap!(startDate); + } else { + final selectedDate = await showDatePicker( + context: context, + initialDate: startDate, + firstDate: _minDate, + lastDate: _maxDate, + ); + + if (selectedDate == null) return; + jumpToWeek(selectedDate); + } + }, + headerStringBuilder: widget.headerStringBuilder, + headerStyle: widget.headerStyle, + ); + } + + HourLinePainter _defaultHourLinePainter( + Color lineColor, + double lineHeight, + double offset, + double minuteHeight, + bool showVerticalLine, + double verticalLineOffset, + LineStyle lineStyle, + double dashWidth, + double dashSpaceWidth, + double emulateVerticalOffsetBy, + int startHour, + int endHour, + ) { + return HourLinePainter( + lineColor: lineColor, + lineHeight: lineHeight, + offset: offset, + minuteHeight: minuteHeight, + verticalLineOffset: verticalLineOffset, + showVerticalLine: showVerticalLine, + lineStyle: lineStyle, + dashWidth: dashWidth, + dashSpaceWidth: dashSpaceWidth, + emulateVerticalOffsetBy: emulateVerticalOffsetBy, + startHour: startHour, + endHour: endHour, + ); + } + + /// Called when user change page using any gesture or inbuilt functions. + void _onPageChange(int index) { + if (mounted) { + setState(() { + _currentStartDate = DateTime( + _currentStartDate.year, + _currentStartDate.month, + _currentStartDate.day + (index - _currentIndex) * widget.daysInView, + ); + _currentEndDate = + _currentStartDate.add(Duration(days: (widget.daysInView - 1))); + _currentIndex = index; + }); + } + widget.onPageChange?.call(_currentStartDate, _currentIndex); + } + + /// Animate to next page + /// + /// Arguments [duration] and [curve] will override default values provided + /// as [DayView.pageTransitionDuration] and [DayView.pageTransitionCurve] + /// respectively. + void nextPage({Duration? duration, Curve? curve}) { + _pageController.nextPage( + duration: duration ?? widget.pageTransitionDuration, + curve: curve ?? widget.pageTransitionCurve, + ); + } + + /// Animate to previous page + /// + /// Arguments [duration] and [curve] will override default values provided + /// as [DayView.pageTransitionDuration] and [DayView.pageTransitionCurve] + /// respectively. + void previousPage({Duration? duration, Curve? curve}) { + _pageController.previousPage( + duration: duration ?? widget.pageTransitionDuration, + curve: curve ?? widget.pageTransitionCurve, + ); + } + + /// Jumps to page number [page] + /// + /// + void jumpToPage(int page) => _pageController.jumpToPage(page); + + /// Animate to page number [page]. + /// + /// Arguments [duration] and [curve] will override default values provided + /// as [DayView.pageTransitionDuration] and [DayView.pageTransitionCurve] + /// respectively. + Future animateToPage(int page, + {Duration? duration, Curve? curve}) async { + await _pageController.animateToPage(page, + duration: duration ?? widget.pageTransitionDuration, + curve: curve ?? widget.pageTransitionCurve); + } + + /// Returns current page number. + int get currentPage => _currentIndex; + + /// Jumps to page which gives day calendar for [week] + void jumpToWeek(DateTime week) { + if (week.isBefore(_minDate) || week.isAfter(_maxDate)) { + throw "Invalid date selected."; + } + + final index = _minDate.getMultiDayDifference( + startDate: _minDate.withoutTime, + endDate: week, + daysInView: widget.daysInView); + _pageController.jumpToPage(index - 1); + } + + /// Animate to page which gives day calendar for [week]. + /// + /// Arguments [duration] and [curve] will override default values provided + /// as [MultiDayView.pageTransitionDuration] and [MultiDayView.pageTransitionCurve] + /// respectively. + Future animateToWeek(DateTime week, + {Duration? duration, Curve? curve}) async { + if (week.isBefore(_minDate) || week.isAfter(_maxDate)) { + throw "Invalid date selected."; + } + await _pageController.animateToPage( + _minDate.getWeekDifference(week, start: WeekDays.monday), + duration: duration ?? widget.pageTransitionDuration, + curve: curve ?? widget.pageTransitionCurve, + ); + } + + /// Returns the current visible week's first date. + DateTime get currentDate => DateTime( + _currentStartDate.year, _currentStartDate.month, _currentStartDate.day); + + /// Jumps to page which contains given events and make event + /// tile visible to user. + /// + Future jumpToEvent(CalendarEventData event) async { + jumpToWeek(event.date); + + await _scrollConfiguration.setScrollEvent( + event: event, + duration: Duration.zero, + curve: Curves.ease, + ); + } + + /// Animate to page which contains given events and make event + /// tile visible to user. + /// + /// Arguments [duration] and [curve] will override default values provided + /// as [DayView.pageTransitionDuration] and [DayView.pageTransitionCurve] + /// respectively. + /// + /// Actual duration will be 2 times the given duration. + /// + /// Ex, If provided duration is 200 milliseconds then this function will take + /// 200 milliseconds for animate to page then 200 milliseconds for + /// scroll to event tile. + /// + /// + Future animateToEvent(CalendarEventData event, + {Duration? duration, Curve? curve}) async { + await animateToWeek(event.date, duration: duration, curve: curve); + await _scrollConfiguration.setScrollEvent( + event: event, + duration: duration ?? widget.pageTransitionDuration, + curve: curve ?? widget.pageTransitionCurve, + ); + } + + /// Animate to specific scroll controller offset + void animateTo( + double offset, { + Duration duration = const Duration(milliseconds: 200), + Curve curve = Curves.linear, + }) { + _scrollController.animateTo( + offset, + duration: duration, + curve: curve, + ); + } + + /// check if any dates contains current date or not. + /// Returns true if it does else false. + bool _showLiveTimeIndicator(List dates) => + dates.any((date) => date.compareWithoutTime(DateTime.now())); + + /// Listener for every week page ScrollController + void _scrollPageListener(ScrollController controller) { + _lastScrollOffset = controller.offset; + } +}