diff --git a/example/lib/enumerations.dart b/example/lib/enumerations.dart index 0cb3c76b..defa4a91 100644 --- a/example/lib/enumerations.dart +++ b/example/lib/enumerations.dart @@ -1 +1 @@ -enum CalendarView { month, day, week } +enum CalendarView { month, day, week, multiDay } diff --git a/example/lib/extension.dart b/example/lib/extension.dart index 2a2b8c4a..603f7833 100644 --- a/example/lib/extension.dart +++ b/example/lib/extension.dart @@ -124,6 +124,22 @@ extension StringExt on String { String get capitalized => toBeginningOfSentenceCase(this) ?? ""; } +extension TimeOfDayExtension on TimeOfDay { + /// Formats the time as a string (e.g., "2:30 PM" or "14:30") + String getTimeInFormat(TimeStampFormat format) { + if (format == TimeStampFormat.parse_12) { + final period = hour >= 12 ? 'PM' : 'AM'; + final displayHour = hour == 0 ? 12 : (hour > 12 ? hour - 12 : hour); + final minuteStr = minute.toString().padLeft(2, '0'); + return '$displayHour:$minuteStr $period'; + } else { + final hourStr = hour.toString().padLeft(2, '0'); + final minuteStr = minute.toString().padLeft(2, '0'); + return '$hourStr:$minuteStr'; + } + } +} + extension ViewNameExt on CalendarView { String get name => toString().split(".").last; } diff --git a/example/lib/l10n/app_ar.arb b/example/lib/l10n/app_ar.arb index ea9d685d..f1b31458 100644 --- a/example/lib/l10n/app_ar.arb +++ b/example/lib/l10n/app_ar.arb @@ -71,5 +71,9 @@ "done": "تم", "selectLanguage": "اختر اللغة", "reachedTheEndPage": "لقد وصلت إلى نهاية الصفحة", - "reachedTheStartPage": "لقد وصلت إلى بداية الصفحة" + "reachedTheStartPage": "لقد وصلت إلى بداية الصفحة", + "annualTechConferenceTitle": "المؤتمر التقني السنوي", + "annualTechConferenceDesc": "حضور المؤتمر التقني السنوي.", + "extendedWorkshopTitle": "ورشة عمل موسعة", + "extendedWorkshopDesc": "المشاركة في ورشة العمل الموسعة لتطوير البرمجيات." } diff --git a/example/lib/l10n/app_en.arb b/example/lib/l10n/app_en.arb index 41ed55a8..dc1a9919 100644 --- a/example/lib/l10n/app_en.arb +++ b/example/lib/l10n/app_en.arb @@ -71,5 +71,9 @@ "done": "Done", "selectLanguage": "Select Language", "reachedTheEndPage": "You have reached the end of the page", - "reachedTheStartPage": "You have reached the start of the page" + "reachedTheStartPage": "You have reached the start of the page", + "annualTechConferenceTitle": "Annual Tech Conference", + "annualTechConferenceDesc": "Attend the annual tech conference.", + "extendedWorkshopTitle": "Extended Workshop", + "extendedWorkshopDesc": "Participate in the extended software development workshop." } diff --git a/example/lib/l10n/app_es.arb b/example/lib/l10n/app_es.arb index 1c2e354c..88ddf1e7 100644 --- a/example/lib/l10n/app_es.arb +++ b/example/lib/l10n/app_es.arb @@ -71,5 +71,9 @@ "done": "Hecho", "selectLanguage": "Seleccionar idioma", "reachedTheEndPage": "Has llegado al final de la página.", - "reachedTheStartPage": "Has llegado al inicio de la página." + "reachedTheStartPage": "Has llegado al inicio de la página.", + "annualTechConferenceTitle": "Conferencia Anual de Tecnología", + "annualTechConferenceDesc": "Asistir a la conferencia anual de tecnología.", + "extendedWorkshopTitle": "Taller Extendido", + "extendedWorkshopDesc": "Participa en el taller extendido de desarrollo de software." } diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index f2a5a781..32879964 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -34,103 +34,97 @@ class _HomePageState extends State { final translate = context.translate; final events = [ - CalendarEventData( - date: _now, + // Example of timeRanged event - for events with specific start/end times on a single day + CalendarEventData.timeRanged( title: translate.projectMeetingTitle, description: translate.projectMeetingDesc, - startTime: DateTime(_now.year, _now.month, _now.day, 18, 30), - endTime: DateTime(_now.year, _now.month, _now.day, 22), + date: _now, + startTime: const TimeOfDay(hour: 18, minute: 30), + endTime: const TimeOfDay(hour: 22, minute: 0), ), - CalendarEventData( - date: _now.subtract(Duration(days: 3)), - recurrenceSettings: RecurrenceSettings.withCalculatedEndDate( - startDate: _now.subtract(Duration(days: 3)), - ), + // Example of wholeDay event with recurrence - for all-day events + CalendarEventData.wholeDay( title: translate.leetcodeContestTitle, description: translate.leetcodeContestDesc, + date: _now.subtract(const Duration(days: 3)), + recurrenceSettings: RecurrenceSettings.withCalculatedEndDate( + startDate: _now.subtract(const Duration(days: 3)), + ), ), - CalendarEventData( - date: _now.subtract(Duration(days: 3)), + // Example of wholeDay event with daily recurrence + CalendarEventData.wholeDay( + title: translate.physicsTestTitle, + description: translate.physicsTestDesc, + date: _now.subtract(const Duration(days: 3)), recurrenceSettings: RecurrenceSettings.withCalculatedEndDate( - startDate: _now.subtract(Duration(days: 3)), + startDate: _now.subtract(const Duration(days: 3)), frequency: RepeatFrequency.daily, recurrenceEndOn: RecurrenceEnd.after, occurrences: 5, ), - title: translate.physicsTestTitle, - description: translate.physicsTestDesc, ), - CalendarEventData( - date: _now.add(Duration(days: 1)), - startTime: DateTime(_now.year, _now.month, _now.day, 18), - endTime: DateTime(_now.year, _now.month, _now.day, 19), + // Example of timeRanged event with recurrence + CalendarEventData.timeRanged( + title: translate.weddingAnniversaryTitle, + description: translate.weddingAnniversaryDesc, + date: _now.add(const Duration(days: 1)), + startTime: const TimeOfDay(hour: 18, minute: 0), + endTime: const TimeOfDay(hour: 19, minute: 0), recurrenceSettings: RecurrenceSettings( startDate: _now, - endDate: _now.add(Duration(days: 5)), + endDate: _now.add(const Duration(days: 5)), frequency: RepeatFrequency.daily, recurrenceEndOn: RecurrenceEnd.after, occurrences: 5, ), - title: translate.weddingAnniversaryTitle, - description: translate.weddingAnniversaryDesc, ), - CalendarEventData( - date: _now, - startTime: DateTime(_now.year, _now.month, _now.day, 14), - endTime: DateTime(_now.year, _now.month, _now.day, 17), + // Example of timeRanged event - afternoon tournament + CalendarEventData.timeRanged( title: translate.footballTournamentTitle, description: translate.footballTournamentDesc, + date: _now, + startTime: const TimeOfDay(hour: 14, minute: 0), + endTime: const TimeOfDay(hour: 17, minute: 0), ), - CalendarEventData( - date: _now.add(Duration(days: 3)), - startTime: DateTime( - _now.add(Duration(days: 3)).year, - _now.add(Duration(days: 3)).month, - _now.add(Duration(days: 3)).day, - 10, - ), - endTime: DateTime( - _now.add(Duration(days: 3)).year, - _now.add(Duration(days: 3)).month, - _now.add(Duration(days: 3)).day, - 14, - ), + // Example of timeRanged event - morning meeting + CalendarEventData.timeRanged( title: translate.sprintMeetingTitle, description: translate.sprintMeetingDesc, + date: _now.add(const Duration(days: 3)), + startTime: const TimeOfDay(hour: 10, minute: 0), + endTime: const TimeOfDay(hour: 14, minute: 0), ), - CalendarEventData( - date: _now.subtract(Duration(days: 2)), - startTime: DateTime( - _now.subtract(Duration(days: 2)).year, - _now.subtract(Duration(days: 2)).month, - _now.subtract(Duration(days: 2)).day, - 14, - ), - endTime: DateTime( - _now.subtract(Duration(days: 2)).year, - _now.subtract(Duration(days: 2)).month, - _now.subtract(Duration(days: 2)).day, - 16, - ), + // Example of timeRanged event - 2 hour meeting + CalendarEventData.timeRanged( title: translate.teamMeetingTitle, description: translate.teamMeetingDesc, + date: _now.subtract(const Duration(days: 2)), + startTime: const TimeOfDay(hour: 14, minute: 0), + endTime: const TimeOfDay(hour: 16, minute: 0), ), - CalendarEventData( - date: _now.subtract(Duration(days: 2)), - startTime: DateTime( - _now.subtract(Duration(days: 2)).year, - _now.subtract(Duration(days: 2)).month, - _now.subtract(Duration(days: 2)).day, - 10, - ), - endTime: DateTime( - _now.subtract(Duration(days: 2)).year, - _now.subtract(Duration(days: 2)).month, - _now.subtract(Duration(days: 2)).day, - 12, - ), + // Example of timeRanged event - chemistry viva + CalendarEventData.timeRanged( title: translate.chemistryVivaTitle, description: translate.chemistryVivaDesc, + date: _now.subtract(const Duration(days: 2)), + startTime: const TimeOfDay(hour: 10, minute: 0), + endTime: const TimeOfDay(hour: 12, minute: 0), + ), + // Example of multiDay event - spanning multiple days without specific times + CalendarEventData.multiDay( + title: translate.annualTechConferenceTitle, + description: translate.annualTechConferenceDesc, + startDate: _now.add(const Duration(days: 5)), + endDate: _now.add(const Duration(days: 7)), + ), + // Example of multiDay event with specific times - workshop spanning multiple days + CalendarEventData.multiDay( + title: translate.extendedWorkshopTitle, + description: translate.extendedWorkshopDesc, + startDate: _now.add(const Duration(days: 10)), + endDate: _now.add(const Duration(days: 12)), + startTime: const TimeOfDay(hour: 9, minute: 0), + endTime: const TimeOfDay(hour: 17, minute: 0), ), ]; _controller!.addAll(events); diff --git a/example/lib/widgets/add_event_form.dart b/example/lib/widgets/add_event_form.dart index e564d43c..d869cd96 100644 --- a/example/lib/widgets/add_event_form.dart +++ b/example/lib/widgets/add_event_form.dart @@ -117,7 +117,12 @@ class _AddOrEditEventFormState extends State { } }); }, - activeThumbColor: color.surface, + activeTrackColor: color.onSurface.accent, + inactiveTrackColor: color.surface, + inactiveThumbColor: color.onSurface, + activeThumbColor: _isRecurring + ? color.onSurface + : color.surface, ), ], ), @@ -370,7 +375,7 @@ class _AddOrEditEventFormState extends State { setState(() { _selectedDays[index] = selected; if (!_selectedDays.contains(true)) { - _selectedDays[_startDate.weekday - 1] = true; + _selectedDays[_startDate.weekDayEnum.index] = true; } }); }, @@ -583,10 +588,20 @@ class _AddOrEditEventFormState extends State { : _endDate; final event = CalendarEventData( - date: _startDate, + startDate: _startDate, endDate: eventEndDate, - endTime: combinedEndTime, - startTime: combinedStartTime, + endTime: combinedEndTime?.hour != null + ? TimeOfDay( + hour: combinedEndTime!.hour, + minute: combinedEndTime.minute, + ) + : null, + startTime: combinedStartTime?.hour != null + ? TimeOfDay( + hour: combinedStartTime!.hour, + minute: combinedStartTime.minute, + ) + : null, color: _color, title: _titleController.text.trim(), description: _descriptionController.text.trim(), @@ -597,15 +612,15 @@ class _AddOrEditEventFormState extends State { _resetForm(); } - /// Get list of weekdays in indices from the selected days - List get _toWeekdayInIndices { - List selectedIndexes = []; + /// Get list of weekdays from the selected days + List get _toWeekdayInIndices { + List selectedWeekdays = []; for (int i = 0; i < _selectedDays.length; i++) { if (_selectedDays[i] == true) { - selectedIndexes.add(i); + selectedWeekdays.add(WeekDays.values[i]); } } - return selectedIndexes; + return selectedWeekdays; } void updateWeekdaysSelection() { @@ -615,14 +630,14 @@ class _AddOrEditEventFormState extends State { } DateTime current = _startDate; while (current.isBefore(_endDate) || current.isAtSameMomentAs(_endDate)) { - _selectedDays[current.weekday - 1] = true; + _selectedDays[current.weekDayEnum.index] = true; current = current.add(Duration(days: 1)); } } /// Set initial selected week to start date void _setInitialWeekday() { - final currentWeekday = DateTime.now().weekday - 1; + final currentWeekday = DateTime.now().weekDayEnum.index; _selectedDays[currentWeekday] = true; } @@ -636,8 +651,18 @@ class _AddOrEditEventFormState extends State { _startDate = event.date; _endDate = event.endDate; - _startTime = event.startTime ?? _startTime; - _endTime = event.endTime ?? _endTime; + _startTime = event.startTime != null + ? _startDate.copyWith( + hour: event.startTime!.hour, + minute: event.startTime!.minute, + ) + : _startDate; + _endTime = event.endTime != null + ? _endDate.copyWith( + hour: event.endTime!.hour, + minute: event.endTime!.minute, + ) + : _endDate; _titleController.text = event.title; _descriptionController.text = event.description ?? ''; _color = event.color; // Set the event color @@ -659,7 +684,7 @@ class _AddOrEditEventFormState extends State { // Clear weekdays selection and then set the selected days _selectedDays = List.filled(7, false); event.recurrenceSettings!.weekdays.forEach( - (index) => _selectedDays[index] = true, + (week) => _selectedDays[week.index] = true, ); } else { _isRecurring = false; diff --git a/example/lib/widgets/calendar_configs.dart b/example/lib/widgets/calendar_configs.dart index 7079de84..4e5ff3e6 100644 --- a/example/lib/widgets/calendar_configs.dart +++ b/example/lib/widgets/calendar_configs.dart @@ -125,6 +125,8 @@ class _CalendarConfigState extends State { break; case CalendarView.week: viewName = translate.weekView; + case CalendarView.multiDay: + viewName = translate.multidayView; break; } return GestureDetector( diff --git a/example/lib/widgets/calendar_views.dart b/example/lib/widgets/calendar_views.dart index 7988893e..a4eebddb 100644 --- a/example/lib/widgets/calendar_views.dart +++ b/example/lib/widgets/calendar_views.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:example/widgets/multi_day_view_widget.dart'; import 'package:flutter/material.dart'; import '../enumerations.dart'; @@ -25,7 +26,9 @@ class CalendarViews extends StatelessWidget { width: double.infinity, color: AppColors.grey, child: Center( - child: view == CalendarView.month + child: view == CalendarView.multiDay + ? MultiDayViewWidget(width: width) + : view == CalendarView.month ? MonthViewWidget(width: width) : view == CalendarView.day ? DayViewWidget(width: width) diff --git a/example/lib/widgets/multi_day_view_widget.dart b/example/lib/widgets/multi_day_view_widget.dart index e2afff8d..5cab1d4c 100644 --- a/example/lib/widgets/multi_day_view_widget.dart +++ b/example/lib/widgets/multi_day_view_widget.dart @@ -16,7 +16,8 @@ class MultiDayViewWidget extends StatelessWidget { daysInView: 3, width: width, showLiveTimeLineInAllDays: true, - eventArranger: SideEventArranger(maxWidth: 30), + eventArranger: SideEventArranger(), + backgroundColor: Colors.white, timeLineWidth: 65, scrollPhysics: const BouncingScrollPhysics(), liveTimeIndicatorSettings: LiveTimeIndicatorSettings( diff --git a/example/lib/widgets/week_view_widget.dart b/example/lib/widgets/week_view_widget.dart index 21bc092e..862ee9f2 100644 --- a/example/lib/widgets/week_view_widget.dart +++ b/example/lib/widgets/week_view_widget.dart @@ -16,7 +16,7 @@ class WeekViewWidget extends StatelessWidget { width: width, showWeekends: true, showLiveTimeLineInAllDays: true, - eventArranger: SideEventArranger(maxWidth: 30), + eventArranger: SideEventArranger(), timeLineWidth: 65, scrollPhysics: const BouncingScrollPhysics(), liveTimeIndicatorSettings: LiveTimeIndicatorSettings( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 14ec6e2c..7f344b8b 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -19,7 +19,7 @@ version: 1.0.0+1 environment: sdk: ">=3.10.4 <4.0.0" - flutter: 3.38.5 + flutter: 3.38.6 dependencies: flutter: diff --git a/lib/src/calendar_event_data.dart b/lib/src/calendar_event_data.dart index e2f025c6..f496f874 100644 --- a/lib/src/calendar_event_data.dart +++ b/lib/src/calendar_event_data.dart @@ -14,14 +14,12 @@ class CalendarEventData { final DateTime date; /// Defines the start time of the event. - /// [endTime] and [startTime] will defines time on same day. /// This is required when you are using [CalendarEventData] for [DayView] or [WeekView] - final DateTime? startTime; + final TimeOfDay? startTime; /// Defines the end time of the event. - /// [endTime] and [startTime] defines time on same day. /// This is required when you are using [CalendarEventData] for [DayView] - final DateTime? endTime; + final TimeOfDay? endTime; /// Title of the event. final String title; @@ -47,21 +45,191 @@ class CalendarEventData { /// Define reoccurrence settings final RecurrenceSettings? recurrenceSettings; - /// {@macro calendar_event_data_doc} - CalendarEventData({ + /// Private constructor for internal use + const CalendarEventData._({ required this.title, - required DateTime date, + required this.date, + required this.startTime, + required this.endTime, + DateTime? endDate, this.description, this.event, this.color = Colors.blue, - this.startTime, - this.endTime, this.titleStyle, this.descriptionStyle, this.recurrenceSettings, + }) : _endDate = endDate; + + /// {@macro calendar_event_data_doc} + /// Creates a basic calendar event. + /// Use type-specific factories ([timeRanged], [wholeDay], [multiDay]) for better clarity. + factory CalendarEventData({ + required String title, + required DateTime startDate, + String? description, + T? event, + Color color = Colors.blue, + TimeOfDay? startTime, + TimeOfDay? endTime, + TextStyle? titleStyle, + TextStyle? descriptionStyle, + RecurrenceSettings? recurrenceSettings, + // End date of the event (only date part is considered) DateTime? endDate, - }) : _endDate = endDate?.withoutTime, - date = date.withoutTime; + }) { + return CalendarEventData._( + title: title, + date: startDate.withoutTime, + startTime: startTime, + endTime: endTime, + endDate: endDate?.withoutTime, + description: description, + event: event, + color: color, + titleStyle: titleStyle, + descriptionStyle: descriptionStyle, + recurrenceSettings: recurrenceSettings, + ); + } + + /// Creates a time-ranged event with specific start and end times on a single day. + /// + /// This factory is ideal for events that occur within a specific time range + /// on a single day (e.g., "Meeting from 2 PM to 4 PM"). + /// + /// Example: + /// ```dart + /// final meeting = CalendarEventData.timeRanged( + /// title: "Team Meeting", + /// date: DateTime(2024, 1, 15), + /// startTime: TimeOfDay(hour: 14, minute: 0), // 2 PM + /// endTime: TimeOfDay(hour: 16, minute: 0), // 4 PM + /// description: "Weekly sync", + /// ); + /// ``` + factory CalendarEventData.timeRanged({ + required String title, + required DateTime date, + required TimeOfDay startTime, + required TimeOfDay endTime, + String? description, + T? event, + Color color = Colors.blue, + TextStyle? titleStyle, + TextStyle? descriptionStyle, + RecurrenceSettings? recurrenceSettings, + }) { + return CalendarEventData._( + title: title, + date: date.withoutTime, + startTime: startTime, + endTime: endTime, + endDate: null, + description: description, + event: event, + color: color, + titleStyle: titleStyle, + descriptionStyle: descriptionStyle, + recurrenceSettings: recurrenceSettings, + ); + } + + /// Creates a whole day event that spans exactly one full day. + /// + /// This factory is ideal for all-day events like holidays, birthdays, + /// or any event that doesn't have specific start/end times. + /// + /// Example: + /// ```dart + /// final holiday = CalendarEventData.wholeDay( + /// title: "Independence Day", + /// date: DateTime(2024, 7, 4), + /// description: "National Holiday", + /// ); + /// ``` + factory CalendarEventData.wholeDay({ + required String title, + required DateTime date, + String? description, + T? event, + Color color = Colors.blue, + TextStyle? titleStyle, + TextStyle? descriptionStyle, + RecurrenceSettings? recurrenceSettings, + }) { + return CalendarEventData._( + title: title, + date: date.withoutTime, + startTime: null, + endTime: null, + endDate: null, + description: description, + event: event, + color: color, + titleStyle: titleStyle, + descriptionStyle: descriptionStyle, + recurrenceSettings: recurrenceSettings, + ); + } + + /// Creates a multi-day event that spans multiple days. + /// + /// This factory supports two modes: + /// 1. **Whole-day multi-day events**: Omit [startTime] and [endTime] + /// 2. **Timed multi-day events**: Provide [startTime] and [endTime] + /// + /// Use this for events like conferences, vacations, workshops, or any event + /// spanning multiple days with or without specific times. + /// + /// Examples: + /// ```dart + /// // Whole-day multi-day event (vacation, conference) + /// final conference = CalendarEventData.multiDay( + /// title: "Tech Conference", + /// startDate: DateTime(2024, 3, 10), + /// endDate: DateTime(2024, 3, 12), + /// description: "3-day tech event", + /// ); + /// + /// // Multi-day event with specific times (e.g., 3 PM on 3rd to 6 PM on 6th) + /// final workshop = CalendarEventData.multiDay( + /// title: "Extended Workshop", + /// startDate: DateTime(2024, 2, 3), + /// endDate: DateTime(2024, 2, 6), + /// startTime: TimeOfDay(hour: 15, minute: 0), // 3 PM + /// endTime: TimeOfDay(hour: 18, minute: 0), // 6 PM + /// description: "Workshop from 3 PM on 3rd to 6 PM on 6th", + /// ); + /// ``` + factory CalendarEventData.multiDay({ + required String title, + // Start date of the multi-day event (only date part is considered) + required DateTime startDate, + // End date of the multi-day event (only date part is considered) + required DateTime endDate, + TimeOfDay? startTime, + TimeOfDay? endTime, + String? description, + T? event, + Color color = Colors.blue, + TextStyle? titleStyle, + TextStyle? descriptionStyle, + RecurrenceSettings? recurrenceSettings, + }) { + return CalendarEventData._( + title: title, + date: startDate.withoutTime, + startTime: startTime, + endTime: endTime, + endDate: endDate.withoutTime, + description: description, + event: event, + color: color, + titleStyle: titleStyle, + descriptionStyle: descriptionStyle, + recurrenceSettings: recurrenceSettings, + ); + } DateTime get endDate => _endDate ?? date; @@ -91,20 +259,49 @@ class CalendarEventData { } Duration get duration { - if (isFullDayEvent) return Duration(days: 1); - - final now = DateTime.now(); - - final end = now.copyFromMinutes(endTime!.getTotalMinutes); - final start = now.copyFromMinutes(startTime!.getTotalMinutes); + if (isFullDayEvent) { + // For full-day events, calculate the number of days between start and end dates + final daysDifference = endDate.difference(date).inDays; + return Duration(days: daysDifference + 1); + } - if (end.isDayStart) { - final difference = - end.add(Duration(days: 1) - Duration(seconds: 1)).difference(start); + // For events with specific times, check if it's a multi-day event + if (isRangingEvent) { + // Multi-day event with specific times + // Calculate duration from startTime on start date to endTime on end date + final startDateTime = DateTime( + date.year, + date.month, + date.day, + startTime!.hour, + startTime!.minute, + ); + final endDateTime = DateTime( + endDate.year, + endDate.month, + endDate.day, + endTime!.hour, + endTime!.minute, + ); + return endDateTime.difference(startDateTime); + } - return difference + Duration(seconds: 1); + // Single-day event with specific times + final startMinutes = startTime!.getTotalMinutes; + final endMinutes = endTime!.getTotalMinutes; + + // If end time is at day start (00:00), treat it as end of day + if (endTime!.isDayStart) { + // Duration from start time to end of day (23:59:59) + final minutesUntilEndOfDay = (24 * 60) - startMinutes; + return Duration(minutes: minutesUntilEndOfDay); + } else if (endMinutes < startMinutes) { + // End time is on the next day + final minutesUntilMidnight = (24 * 60) - startMinutes; + return Duration(minutes: minutesUntilMidnight + endMinutes); } else { - return end.difference(start); + // Same day event + return Duration(minutes: endMinutes - startMinutes); } } @@ -138,8 +335,8 @@ class CalendarEventData { String? description, T? event, Color? color, - DateTime? startTime, - DateTime? endTime, + TimeOfDay? startTime, + TimeOfDay? endTime, TextStyle? titleStyle, TextStyle? descriptionStyle, DateTime? endDate, @@ -148,7 +345,7 @@ class CalendarEventData { }) { return CalendarEventData( title: title ?? this.title, - date: date ?? this.date, + startDate: date ?? this.date, startTime: startTime ?? this.startTime, endTime: endTime ?? this.endTime, color: color ?? this.color, @@ -174,11 +371,11 @@ class CalendarEventData { ((startTime == null && other.startTime == null) || (startTime != null && other.startTime != null && - startTime!.hasSameTimeAs(other.startTime!))) && + startTime!.isSameAs(other.startTime!))) && ((endTime == null && other.endTime == null) || (endTime != null && other.endTime != null && - endTime!.hasSameTimeAs(other.endTime!))) && + endTime!.isSameAs(other.endTime!))) && title == other.title && color == other.color && titleStyle == other.titleStyle && @@ -197,25 +394,19 @@ class CalendarEventData { } /// {@template calendar_event_data_doc} -/// Stores all the events on [date]. +/// Stores the event data. +/// +/// [date] and [endDate] define the date range of the event. +/// [startTime] and [endTime] define the time of the event on the start and end dates respectively. /// -/// If [startTime] and [endTime] both are 0 or either of them is null, then -/// event will be considered a full day event. +/// If [startTime] and [endTime] are null, the event is considered a full day event. /// -/// - [date] and [endDate] are used to define dates only. So, If you -/// are providing any time information with these two arguments, -/// it will be ignored. +/// - [date] and [endDate] are used to define dates only. Any time information +/// provided with these arguments is ignored. /// -/// - [startTime] and [endTime] are used to define the time span of the event. -/// So, If you are providing any day information (year, month, day), it will -/// be ignored. It will also, consider only hour and minutes as time. So, -/// seconds, milliseconds and microseconds will be ignored as well. +/// - [startTime] and [endTime] are of type [TimeOfDay], considering only hours and minutes. /// -/// - [startTime] and [endTime] can not span more then one day. For example, -/// If start time is 11th Nov 11:30 PM and end time is 12th Nov 1:30 AM, it -/// will not be considered as valid time. Because for [startTime] and [endTime], -/// day will be ignored so, 11:30 PM ([startTime]) occurs after -/// 1:30 AM ([endTime]). Events with invalid time will throw -/// [AssertionError] in debug mode and will be ignored in release mode -/// in [DayView] and [WeekView]. +/// - For multi-day events, [date] defines the start date and [endDate] defines +/// the end date. The [startTime] applies to the start date and +/// [endTime] applies to the end date. /// {@endtemplate} diff --git a/lib/src/event_arrangers/merge_event_arranger.dart b/lib/src/event_arrangers/merge_event_arranger.dart index 59dfed4a..007a3d6e 100644 --- a/lib/src/event_arrangers/merge_event_arranger.dart +++ b/lib/src/event_arrangers/merge_event_arranger.dart @@ -152,8 +152,10 @@ class MergeEventArranger extends EventArranger { bottom: bottom, left: 0, right: 0, - startDuration: startTime.copyFromMinutes(eventStart), - endDuration: endTime.copyFromMinutes(eventEnd), + startDuration: TimeOfDayExtension.copyFromMinutes(eventStart) + .toDateTime(calendarViewDate), + endDuration: TimeOfDayExtension.copyFromMinutes(eventEnd) + .toDateTime(calendarViewDate), events: [event], calendarViewDate: calendarViewDate, ); diff --git a/lib/src/event_arrangers/side_event_arranger.dart b/lib/src/event_arrangers/side_event_arranger.dart index e960421b..1075d4ec 100644 --- a/lib/src/event_arrangers/side_event_arranger.dart +++ b/lib/src/event_arrangers/side_event_arranger.dart @@ -181,8 +181,10 @@ class SideEventArranger extends EventArranger { right: totalWidth - (offset + slotWidth), top: top, bottom: bottom, - startDuration: startTime.copyFromMinutes(eventStart), - endDuration: endTime.copyFromMinutes(eventEnd), + startDuration: TimeOfDayExtension.copyFromMinutes(eventStart) + .toDateTime(calendarViewDate), + endDuration: TimeOfDayExtension.copyFromMinutes(eventEnd) + .toDateTime(calendarViewDate), events: [e], calendarViewDate: calendarViewDate, ); diff --git a/lib/src/event_controller.dart b/lib/src/event_controller.dart index c4c33f47..f0f3dcbb 100644 --- a/lib/src/event_controller.dart +++ b/lib/src/event_controller.dart @@ -456,7 +456,7 @@ class CalendarData { // Adjust weekday to zero-based indexing and // check if date’s weekday is in the recurrence weekdays final isMatchingWeekday = - recurrenceSettings.weekdays.contains(currentDate.weekday - 1); + recurrenceSettings.weekdays.contains(currentDate.weekDayEnum); final recurrenceEndDate = recurrenceSettings.endDate; if (!isMatchingWeekday) { diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index 870dbb64..9557bc53 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -208,6 +208,42 @@ extension DateTimeExtensions on DateTime { "This extension is not being used in this package and will be removed " "in next major release. Please use withoutTime instead.") DateTime get dateYMD => DateTime(year, month, day); + + /// Returns the corresponding [WeekDays] enum value for this DateTime's weekday. + /// + /// This provides an ergonomic way to convert DateTime.weekday (1-7, Monday-Sunday) + /// to the [WeekDays] enum (0-6, monday-sunday). + /// + /// Example: + /// ```dart + /// final date = DateTime(2024, 1, 15); // Monday + /// print(date.weekDayEnum); // WeekDays.monday + /// ``` + WeekDays get weekDayEnum => WeekDays.values[weekday - 1]; +} + +extension TimeOfDayExtension on TimeOfDay { + /// Returns true if this time represents the start of day (00:00). + bool get isDayStart => hour == 0 && minute == 0; + int get getTotalMinutes => hour * 60 + minute; + + /// Converts TimeOfDay to DateTime on the given date. + DateTime toDateTime(DateTime date) { + return DateTime(date.year, date.month, date.day, hour, minute); + } + + /// Compares if two TimeOfDay objects represent the same time. + bool isSameAs(TimeOfDay other) { + return hour == other.hour && minute == other.minute; + } + + /// Creates a TimeOfDay from total minutes since midnight + /// For example, 870 minutes = 14:30 (2:30 PM) + static TimeOfDay copyFromMinutes(int totalMinutes) { + final hours = totalMinutes ~/ 60; + final minutes = totalMinutes % 60; + return TimeOfDay(hour: hours % 24, minute: minutes); + } } extension ColorExtension on Color { @@ -281,10 +317,6 @@ int defaultEventSorter( (b.startTime?.getTotalMinutes ?? 0); } -extension TimerOfDayExtension on TimeOfDay { - int get getTotalMinutes => hour * 60 + minute; -} - extension IntExtension on int { String appendLeadingZero() { return toString().padLeft(2, '0'); @@ -297,8 +329,8 @@ void debugLog(String message) { debugPrint(message); } catch (e) {} //ignore: empty_catches Suppress exception... - return false; - }(), ''); + return true; + }()); } /// For callbacks with one argument diff --git a/lib/src/modals.dart b/lib/src/modals.dart index 79213bd0..d55f4a44 100644 --- a/lib/src/modals.dart +++ b/lib/src/modals.dart @@ -129,8 +129,8 @@ class RecurrenceSettings { this.frequency = RepeatFrequency.doNotRepeat, this.recurrenceEndOn = RecurrenceEnd.never, this.excludeDates, - List? weekdays, - }) : weekdays = weekdays ?? [startDate.weekday]; + List? weekdays, + }) : weekdays = weekdays ?? [startDate.weekDayEnum]; /// If recurrence event does not have an end date it will calculate end date /// from the start date. @@ -151,8 +151,8 @@ class RecurrenceSettings { this.frequency = RepeatFrequency.doNotRepeat, this.recurrenceEndOn = RecurrenceEnd.never, this.excludeDates, - List? weekdays, - }) : weekdays = weekdays ?? [startDate.weekday] { + List? weekdays, + }) : weekdays = weekdays ?? [startDate.weekDayEnum] { this.endDate = endDate ?? _getEndDate(startDate); } @@ -161,7 +161,7 @@ class RecurrenceSettings { final int? occurrences; final RepeatFrequency frequency; final RecurrenceEnd recurrenceEndOn; - final List weekdays; + final List weekdays; final List? excludeDates; // For recurrence patterns other than weekly, where the event may not repeat @@ -231,19 +231,20 @@ class RecurrenceSettings { var currentDate = startDate; // Check if the start date is one of the recurring weekdays - if (sortedWeekdays.contains(startDate.weekday - 1)) { + if (sortedWeekdays.contains(startDate.weekDayEnum)) { remainingOccurrences--; } while (remainingOccurrences > 0) { // Find the next valid weekday final nextWeekday = sortedWeekdays.firstWhere( - (day) => day > currentDate.weekday - 1, + (day) => day.index > currentDate.weekDayEnum.index, orElse: () => sortedWeekdays.first, ); // Calculate the days to the next occurrence - final daysToAdd = (nextWeekday - (currentDate.weekday - 1) + 7) % 7; + final daysToAdd = + (nextWeekday.index - currentDate.weekDayEnum.index + 7) % 7; // Move the current date to the next occurrence currentDate = currentDate.add(Duration(days: daysToAdd)); @@ -348,7 +349,7 @@ class RecurrenceSettings { int? occurrences, RepeatFrequency? frequency, RecurrenceEnd? recurrenceEndOn, - List? weekdays, + List? weekdays, List? excludeDates, }) { return RecurrenceSettings( diff --git a/lib/src/month_view/month_view.dart b/lib/src/month_view/month_view.dart index b69b1f8b..b7b055a0 100644 --- a/lib/src/month_view/month_view.dart +++ b/lib/src/month_view/month_view.dart @@ -246,8 +246,8 @@ class MonthViewState extends State> { (index) => Expanded( child: SizedBox( width: _cellWidth, - child: - _weekBuilder(weekDays[index].weekday - 1), + child: _weekBuilder( + weekDays[index].weekDayEnum.index), ), ), ), diff --git a/lib/src/multi_day_view/multi_day_view.dart b/lib/src/multi_day_view/multi_day_view.dart index 6619b9f2..982a00a2 100644 --- a/lib/src/multi_day_view/multi_day_view.dart +++ b/lib/src/multi_day_view/multi_day_view.dart @@ -793,8 +793,8 @@ class MultiDayViewState extends State> { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - widget.weekDayStringBuilder?.call(date.weekday - 1) ?? - PackageStrings.currentLocale.weekdays[date.weekday - 1], + widget.weekDayStringBuilder?.call(date.weekDayEnum.index) ?? + PackageStrings.currentLocale.weekdays[date.weekDayEnum.index], style: TextStyle( color: textColor, ), diff --git a/lib/src/week_view/week_view.dart b/lib/src/week_view/week_view.dart index de065c1c..e05ea907 100644 --- a/lib/src/week_view/week_view.dart +++ b/lib/src/week_view/week_view.dart @@ -772,8 +772,8 @@ class WeekViewState extends State> { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - widget.weekDayStringBuilder?.call(date.weekday - 1) ?? - PackageStrings.currentLocale.weekdays[date.weekday - 1], + widget.weekDayStringBuilder?.call(date.weekDayEnum.index) ?? + PackageStrings.currentLocale.weekdays[date.weekDayEnum.index], style: TextStyle( color: context.weekViewColors.weekDayTextColor, ), diff --git a/test/custom_sort_test.dart b/test/custom_sort_test.dart index 0829b0b3..d018a69c 100644 --- a/test/custom_sort_test.dart +++ b/test/custom_sort_test.dart @@ -1,43 +1,43 @@ import 'package:calendar_view/calendar_view.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('Custom sort', () { final date = DateTime(2024, 01, 01); - const oneHour = Duration(hours: 1); /// The bool value indicates if the event is "important" or "regular". final first = CalendarEventData( title: 'Regular event - first', event: false, - date: date, - startTime: date.add(oneHour), - endTime: date.add(oneHour * 2), + startDate: date, + startTime: TimeOfDay(hour: 1, minute: 0), + endTime: TimeOfDay(hour: 2, minute: 0), ); final second = CalendarEventData( title: 'Important event - second', event: true, - date: date, - startTime: date.add(oneHour * 2), - endTime: date.add(oneHour * 3), + startDate: date, + startTime: TimeOfDay(hour: 2, minute: 0), + endTime: TimeOfDay(hour: 3, minute: 0), ); final third = CalendarEventData( title: 'Important event - third', event: true, - date: date, - startTime: date.add(oneHour * 3), - endTime: date.add(oneHour * 4), + startDate: date, + startTime: TimeOfDay(hour: 3, minute: 0), + endTime: TimeOfDay(hour: 4, minute: 0), ); final fourth = CalendarEventData( title: 'Regular event - fourth', event: false, - date: date, - startTime: date.add(oneHour * 4), - endTime: date.add(oneHour * 5), + startDate: date, + startTime: TimeOfDay(hour: 4, minute: 0), + endTime: TimeOfDay(hour: 5, minute: 0), ); /// Events are in random order diff --git a/test/event_arranger_test/merge_event_arranger_test.dart b/test/event_arranger_test/merge_event_arranger_test.dart index 0d9f71cd..3274454f 100644 --- a/test/event_arranger_test/merge_event_arranger_test.dart +++ b/test/event_arranger_test/merge_event_arranger_test.dart @@ -1,4 +1,5 @@ import 'package:calendar_view/calendar_view.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; const height = 1440.0; @@ -12,45 +13,35 @@ void main() { group('MergeEventArrangerTest', () { test('Events which does not overlap.', () { final events = [ - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 1', date: now, - startTime: now.add(Duration(hours: 1)), - endTime: now.add( - Duration(hours: 2), - ), + startTime: TimeOfDay(hour: 1, minute: 0), + endTime: TimeOfDay(hour: 2, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 2', date: now, - startTime: now.add(Duration(hours: 2, minutes: 15)), - endTime: now.add( - Duration(hours: 3), - ), + startTime: TimeOfDay(hour: 2, minute: 15), + endTime: TimeOfDay(hour: 3, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 3', date: now, - startTime: now.add(Duration(hours: 3, minutes: 15)), - endTime: now.add( - Duration(hours: 4), - ), + startTime: TimeOfDay(hour: 3, minute: 15), + endTime: TimeOfDay(hour: 4, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 4', date: now, - startTime: now.add(Duration(hours: 4, minutes: 15)), - endTime: now.add( - Duration(hours: 5), - ), + startTime: TimeOfDay(hour: 4, minute: 15), + endTime: TimeOfDay(hour: 5, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 5', date: now, - startTime: now.add(Duration(hours: 10)), - endTime: now.add( - Duration(hours: 13), - ), + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 13, minute: 0), ), ]; @@ -65,23 +56,20 @@ void main() { expect(mergedEvents.length, events.length); }); + test('Only start time is overlapping', () { final events = [ - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 1', date: now, - startTime: now.add(Duration(hours: 10)), - endTime: now.add( - Duration(hours: 12), - ), + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 12, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 2', date: now, - startTime: now.add(Duration(hours: 8)), - endTime: now.add( - Duration(hours: 10), - ), + startTime: TimeOfDay(hour: 8, minute: 0), + endTime: TimeOfDay(hour: 10, minute: 0), ), ]; @@ -96,23 +84,20 @@ void main() { expect(mergedEvents.length, 1); }); + test('Only end time is overlapping', () { final events = [ - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 1', date: now, - startTime: now.add(Duration(hours: 8)), - endTime: now.add( - Duration(hours: 10), - ), + startTime: TimeOfDay(hour: 8, minute: 0), + endTime: TimeOfDay(hour: 10, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 2', date: now, - startTime: now.add(Duration(hours: 10)), - endTime: now.add( - Duration(hours: 12), - ), + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 12, minute: 0), ), ]; @@ -127,23 +112,20 @@ void main() { expect(mergedEvents.length, 1); }); + test('Event1 is smaller than event 2 and overlapping', () { final events = [ - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 1', date: now, - startTime: now.add(Duration(hours: 10)), - endTime: now.add( - Duration(hours: 12), - ), + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 12, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 2', date: now, - startTime: now.add(Duration(hours: 8)), - endTime: now.add( - Duration(hours: 14), - ), + startTime: TimeOfDay(hour: 8, minute: 0), + endTime: TimeOfDay(hour: 14, minute: 0), ), ]; @@ -158,23 +140,20 @@ void main() { expect(mergedEvents.length, 1); }); + test('Event2 is smaller than event 1 and overlapping', () { final events = [ - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 1', date: now, - startTime: now.add(Duration(hours: 8)), - endTime: now.add( - Duration(hours: 14), - ), + startTime: TimeOfDay(hour: 8, minute: 0), + endTime: TimeOfDay(hour: 14, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 2', date: now, - startTime: now.add(Duration(hours: 10)), - endTime: now.add( - Duration(hours: 12), - ), + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 12, minute: 0), ), ]; @@ -189,23 +168,20 @@ void main() { expect(mergedEvents.length, 1); }); + test('Both events are of same duration and occurs at the same time', () { final events = [ - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 1', date: now, - startTime: now.add(Duration(hours: 10)), - endTime: now.add( - Duration(hours: 12), - ), + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 12, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 2', date: now, - startTime: now.add(Duration(hours: 10)), - endTime: now.add( - Duration(hours: 12), - ), + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 12, minute: 0), ), ]; @@ -220,55 +196,44 @@ void main() { expect(mergedEvents.length, 1); }); + test('Only few events overlaps', () { final events = [ - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 1', date: now, - startTime: now.add(Duration(hours: 1)), - endTime: now.add( - Duration(hours: 2), - ), + startTime: TimeOfDay(hour: 1, minute: 0), + endTime: TimeOfDay(hour: 2, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 4', date: now, - startTime: now.add(Duration(hours: 7)), - endTime: now.add( - Duration(hours: 11), - ), + startTime: TimeOfDay(hour: 7, minute: 0), + endTime: TimeOfDay(hour: 11, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 6', date: now, - startTime: now.add(Duration(hours: 3)), - endTime: now.add( - Duration(hours: 3, minutes: 30), - ), + startTime: TimeOfDay(hour: 3, minute: 0), + endTime: TimeOfDay(hour: 3, minute: 30), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 5', date: now, - startTime: now.add(Duration(hours: 1, minutes: 15)), - endTime: now.add( - Duration(hours: 2, minutes: 15), - ), + startTime: TimeOfDay(hour: 1, minute: 15), + endTime: TimeOfDay(hour: 2, minute: 15), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 3', date: now, - startTime: now.add(Duration(hours: 5)), - endTime: now.add( - Duration(hours: 6), - ), + startTime: TimeOfDay(hour: 5, minute: 0), + endTime: TimeOfDay(hour: 6, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 2', date: now, - startTime: now.add(Duration(hours: 8)), - endTime: now.add( - Duration(hours: 9), - ), + startTime: TimeOfDay(hour: 8, minute: 0), + endTime: TimeOfDay(hour: 9, minute: 0), ), ]; @@ -283,55 +248,44 @@ void main() { expect(mergedEvents.length, 4); }); + test('All events overlaps with each other', () { final events = [ - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 1', date: now, - startTime: now.add(Duration(hours: 1)), - endTime: now.add( - Duration(hours: 2), - ), + startTime: TimeOfDay(hour: 1, minute: 0), + endTime: TimeOfDay(hour: 2, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 2', date: now, - startTime: now.add(Duration(hours: 4)), - endTime: now.add( - Duration(hours: 5), - ), + startTime: TimeOfDay(hour: 4, minute: 0), + endTime: TimeOfDay(hour: 5, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 3', date: now, - startTime: now.add(Duration(hours: 2)), - endTime: now.add( - Duration(hours: 6), - ), + startTime: TimeOfDay(hour: 2, minute: 0), + endTime: TimeOfDay(hour: 6, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 4', date: now, - startTime: now.add(Duration(hours: 7)), - endTime: now.add( - Duration(hours: 10), - ), + startTime: TimeOfDay(hour: 7, minute: 0), + endTime: TimeOfDay(hour: 10, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 5', date: now, - startTime: now.add(Duration(hours: 5)), - endTime: now.add( - Duration(hours: 7), - ), + startTime: TimeOfDay(hour: 5, minute: 0), + endTime: TimeOfDay(hour: 7, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 6', date: now, - startTime: now.add(Duration(hours: 3)), - endTime: now.add( - Duration(hours: 6), - ), + startTime: TimeOfDay(hour: 3, minute: 0), + endTime: TimeOfDay(hour: 6, minute: 0), ), ]; @@ -353,21 +307,17 @@ void main() { group('Edge event should not merge', () { test('End of Event 1 and Start of Event 2 is same', () { final events = [ - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 1', date: now, - startTime: now.add(Duration(hours: 1)), - endTime: now.add( - Duration(hours: 2), - ), + startTime: TimeOfDay(hour: 1, minute: 0), + endTime: TimeOfDay(hour: 2, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 2', date: now, - startTime: now.add(Duration(hours: 2)), - endTime: now.add( - Duration(hours: 3), - ), + startTime: TimeOfDay(hour: 2, minute: 0), + endTime: TimeOfDay(hour: 3, minute: 0), ), ]; @@ -388,21 +338,17 @@ void main() { test('Start of Event 1 and End of Event 2 is same', () { final events = [ - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 1', date: now, - startTime: now.add(Duration(hours: 2)), - endTime: now.add( - Duration(hours: 3), - ), + startTime: TimeOfDay(hour: 2, minute: 0), + endTime: TimeOfDay(hour: 3, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 2', date: now, - startTime: now.add(Duration(hours: 1)), - endTime: now.add( - Duration(hours: 2), - ), + startTime: TimeOfDay(hour: 1, minute: 0), + endTime: TimeOfDay(hour: 2, minute: 0), ), ]; @@ -421,24 +367,21 @@ void main() { expect(mergedEvents.length, 2); }); }); + group('Edge event should merge', () { test('End of Event 1 and Start of Event 2 is same', () { final events = [ - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 1', date: now, - startTime: now.add(Duration(hours: 1)), - endTime: now.add( - Duration(hours: 2), - ), + startTime: TimeOfDay(hour: 1, minute: 0), + endTime: TimeOfDay(hour: 2, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 2', date: now, - startTime: now.add(Duration(hours: 2)), - endTime: now.add( - Duration(hours: 3), - ), + startTime: TimeOfDay(hour: 2, minute: 0), + endTime: TimeOfDay(hour: 3, minute: 0), ), ]; @@ -459,21 +402,17 @@ void main() { test('Start of Event 1 and End of Event 2 is same', () { final events = [ - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 1', date: now, - startTime: now.add(Duration(hours: 2)), - endTime: now.add( - Duration(hours: 3), - ), + startTime: TimeOfDay(hour: 2, minute: 0), + endTime: TimeOfDay(hour: 3, minute: 0), ), - CalendarEventData( + CalendarEventData.timeRanged( title: 'Event 2', date: now, - startTime: now.add(Duration(hours: 1)), - endTime: now.add( - Duration(hours: 2), - ), + startTime: TimeOfDay(hour: 1, minute: 0), + endTime: TimeOfDay(hour: 2, minute: 0), ), ]; @@ -493,6 +432,573 @@ void main() { }); }); + group('Multi-day events', () { + test('Multi-day event with specific times', () { + final events = [ + CalendarEventData.multiDay( + title: 'Workshop', + startDate: now, + endDate: now.add(Duration(days: 2)), + startTime: TimeOfDay(hour: 9, minute: 0), + endTime: TimeOfDay(hour: 17, minute: 0), + ), + ]; + + final mergedEvents = MergeEventArranger().arrange( + events: events, + height: height, + width: width, + heightPerMinute: heightPerMinute, + startHour: startHour, + calendarViewDate: now, + ); + + expect(mergedEvents.length, 1); + }); + }); + // TODO: add tests for the events where start or end time is not valid. }); + + group('CalendarEventData Constructor Tests', () { + group('General Constructor', () { + test('should create event with all parameters', () { + final event = CalendarEventData( + title: 'Test Event', + startDate: now, + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 12, minute: 0), + description: 'Test Description', + color: Colors.red, + ); + + expect(event.title, 'Test Event'); + expect(event.date, now); + expect(event.startTime, TimeOfDay(hour: 10, minute: 0)); + expect(event.endTime, TimeOfDay(hour: 12, minute: 0)); + expect(event.description, 'Test Description'); + expect(event.color, Colors.red); + }); + + test('should create event with minimal parameters', () { + final event = CalendarEventData( + title: 'Minimal Event', + startDate: now, + ); + + expect(event.title, 'Minimal Event'); + expect(event.date, now); + expect(event.startTime, isNull); + expect(event.endTime, isNull); + expect(event.color, Colors.blue); + }); + + test('should strip time from startDate', () { + final dateWithTime = DateTime(2024, 1, 15, 10, 30); + final event = CalendarEventData( + title: 'Event', + startDate: dateWithTime, + ); + + expect(event.date.hour, 0); + expect(event.date.minute, 0); + expect(event.date.second, 0); + expect(event.date.millisecond, 0); + }); + }); + + group('CalendarEventData.timeRanged Constructor', () { + test('should create time-ranged event on single day', () { + final event = CalendarEventData.timeRanged( + title: 'Meeting', + date: now, + startTime: TimeOfDay(hour: 14, minute: 0), + endTime: TimeOfDay(hour: 16, minute: 0), + description: 'Team meeting', + ); + + expect(event.title, 'Meeting'); + expect(event.date, now); + expect(event.startTime, TimeOfDay(hour: 14, minute: 0)); + expect(event.endTime, TimeOfDay(hour: 16, minute: 0)); + expect(event.isFullDayEvent, false); + expect(event.isRangingEvent, false); + }); + + test('should not be a full day event', () { + final event = CalendarEventData.timeRanged( + title: 'Meeting', + date: now, + startTime: TimeOfDay(hour: 9, minute: 0), + endTime: TimeOfDay(hour: 17, minute: 0), + ); + + expect(event.isFullDayEvent, false); + }); + + test('should not be a ranging event', () { + final event = CalendarEventData.timeRanged( + title: 'Meeting', + date: now, + startTime: TimeOfDay(hour: 9, minute: 0), + endTime: TimeOfDay(hour: 17, minute: 0), + ); + + expect(event.isRangingEvent, false); + expect(event.endDate, now); + }); + }); + + group('CalendarEventData.wholeDay Constructor', () { + test('should create whole day event', () { + final event = CalendarEventData.wholeDay( + title: 'Holiday', + date: now, + description: 'National Holiday', + ); + + expect(event.title, 'Holiday'); + expect(event.date, now); + expect(event.startTime, isNull); + expect(event.endTime, isNull); + expect(event.isFullDayEvent, true); + }); + + test('should be a full day event', () { + final event = CalendarEventData.wholeDay( + title: 'Birthday', + date: now, + ); + + expect(event.isFullDayEvent, true); + }); + + test('should not be a ranging event for single day', () { + final event = CalendarEventData.wholeDay( + title: 'Holiday', + date: now, + ); + + expect(event.isRangingEvent, false); + expect(event.endDate, now); + }); + }); + + group('CalendarEventData.multiDay Constructor', () { + test('should create whole-day multi-day event', () { + final startDate = now; + final endDate = now.add(Duration(days: 2)); + + final event = CalendarEventData.multiDay( + title: 'Conference', + startDate: startDate, + endDate: endDate, + description: '3-day conference', + ); + + expect(event.title, 'Conference'); + expect(event.date, startDate); + expect(event.endDate, endDate); + expect(event.startTime, isNull); + expect(event.endTime, isNull); + expect(event.isFullDayEvent, true); + expect(event.isRangingEvent, false); + }); + + test('should create timed multi-day event', () { + final startDate = now; + final endDate = now.add(Duration(days: 3)); + + final event = CalendarEventData.multiDay( + title: 'Workshop', + startDate: startDate, + endDate: endDate, + startTime: TimeOfDay(hour: 9, minute: 0), + endTime: TimeOfDay(hour: 17, minute: 0), + description: 'Extended workshop', + ); + + expect(event.title, 'Workshop'); + expect(event.date, startDate); + expect(event.endDate, endDate); + expect(event.startTime, TimeOfDay(hour: 9, minute: 0)); + expect(event.endTime, TimeOfDay(hour: 17, minute: 0)); + expect(event.isFullDayEvent, false); + expect(event.isRangingEvent, true); + }); + + test('should strip time from both dates', () { + final startDateWithTime = DateTime(2024, 1, 15, 10, 30); + final endDateWithTime = DateTime(2024, 1, 17, 15, 45); + + final event = CalendarEventData.multiDay( + title: 'Event', + startDate: startDateWithTime, + endDate: endDateWithTime, + ); + + expect(event.date.hour, 0); + expect(event.date.minute, 0); + expect(event.endDate.hour, 0); + expect(event.endDate.minute, 0); + }); + }); + }); + + group('CalendarEventData Duration Tests', () { + group('Single-day event durations', () { + test('should calculate duration for same-day event', () { + final event = CalendarEventData.timeRanged( + title: 'Meeting', + date: now, + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 12, minute: 0), + ); + + expect(event.duration.inHours, 2); + expect(event.duration.inMinutes, 120); + }); + + test('should calculate duration for event with minutes', () { + final event = CalendarEventData.timeRanged( + title: 'Quick Meeting', + date: now, + startTime: TimeOfDay(hour: 14, minute: 30), + endTime: TimeOfDay(hour: 15, minute: 45), + ); + + expect(event.duration.inMinutes, 75); + }); + + test( + 'should calculate duration when endTime is before startTime (next day)', + () { + final event = CalendarEventData( + title: 'Night Shift', + startDate: now, + startTime: TimeOfDay(hour: 22, minute: 0), + endTime: TimeOfDay(hour: 6, minute: 0), + ); + + expect(event.duration.inHours, 8); + }); + + test('should handle midnight (00:00) as end time', () { + final event = CalendarEventData( + title: 'Event until midnight', + startDate: now, + startTime: TimeOfDay(hour: 18, minute: 0), + endTime: TimeOfDay(hour: 0, minute: 0), + ); + + expect(event.duration.inMinutes, 360); + }); + }); + + group('Full-day event durations', () { + test('should return 1 day duration for single whole-day event', () { + final event = CalendarEventData.wholeDay( + title: 'Holiday', + date: now, + ); + + expect(event.duration.inDays, 1); + expect(event.duration.inHours, 24); + }); + + test('should calculate correct duration for multi-day whole-day event', + () { + final event = CalendarEventData.multiDay( + title: 'Conference', + startDate: now, + endDate: now.add(Duration(days: 2)), + ); + + expect(event.duration.inDays, 3); + }); + + test('should calculate duration for 5-day conference', () { + final event = CalendarEventData.multiDay( + title: 'Week-long Conference', + startDate: DateTime(2024, 1, 1), + endDate: DateTime(2024, 1, 5), + ); + + expect(event.duration.inDays, 5); + }); + }); + + group('Multi-day event with times (ranging events)', () { + test( + 'should calculate duration for multi-day event with times - reviewer case', + () { + final event = CalendarEventData.multiDay( + title: 'Extended Workshop', + startDate: DateTime(2024, 1, 1), + endDate: DateTime(2024, 1, 3), + startTime: TimeOfDay(hour: 9, minute: 0), + endTime: TimeOfDay(hour: 17, minute: 0), + ); + + expect(event.isRangingEvent, true); + expect(event.duration.inHours, 56); + expect(event.duration.inMinutes, 3360); + }); + + test('should calculate duration for 2-day event with same times', () { + final event = CalendarEventData.multiDay( + title: 'Workshop', + startDate: DateTime(2024, 1, 1), + endDate: DateTime(2024, 1, 2), + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 10, minute: 0), + ); + + expect(event.duration.inHours, 24); + }); + + test( + 'should calculate duration for multi-day event ending before start time', + () { + final event = CalendarEventData.multiDay( + title: 'Event', + startDate: DateTime(2024, 1, 1), + endDate: DateTime(2024, 1, 3), + startTime: TimeOfDay(hour: 15, minute: 0), + endTime: TimeOfDay(hour: 10, minute: 0), + ); + + expect(event.duration.inHours, 43); + }); + + test('should calculate duration for week-long event with specific times', + () { + final event = CalendarEventData.multiDay( + title: 'Training', + startDate: DateTime(2024, 3, 4), + endDate: DateTime(2024, 3, 8), + startTime: TimeOfDay(hour: 9, minute: 0), + endTime: TimeOfDay(hour: 17, minute: 0), + ); + + expect(event.duration.inHours, 104); + }); + + test('should handle multi-day event with minutes precision', () { + final event = CalendarEventData.multiDay( + title: 'Precise Event', + startDate: DateTime(2024, 2, 10), + endDate: DateTime(2024, 2, 12), + startTime: TimeOfDay(hour: 14, minute: 30), + endTime: TimeOfDay(hour: 16, minute: 45), + ); + + expect(event.duration.inMinutes, 3015); + }); + }); + }); + + group('CalendarEventData Property Tests', () { + test('isFullDayEvent should return true when no times provided', () { + final event = CalendarEventData( + title: 'All Day', + startDate: now, + ); + + expect(event.isFullDayEvent, true); + }); + + test('isFullDayEvent should return true when both times are midnight', () { + final event = CalendarEventData( + title: 'Event', + startDate: now, + startTime: TimeOfDay(hour: 0, minute: 0), + endTime: TimeOfDay(hour: 0, minute: 0), + ); + + expect(event.isFullDayEvent, true); + }); + + test('isRangingEvent should return true for multi-day timed event', () { + final event = CalendarEventData.multiDay( + title: 'Multi-day', + startDate: now, + endDate: now.add(Duration(days: 2)), + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 16, minute: 0), + ); + + expect(event.isRangingEvent, true); + }); + + test('isRangingEvent should return false for multi-day full-day event', () { + final event = CalendarEventData.multiDay( + title: 'Multi-day Full Day', + startDate: now, + endDate: now.add(Duration(days: 2)), + ); + + expect(event.isRangingEvent, false); + }); + + test('isRecurringEvent should return false when no recurrence settings', + () { + final event = CalendarEventData( + title: 'One-time Event', + startDate: now, + ); + + expect(event.isRecurringEvent, false); + }); + + test('isRecurringEvent should return true with recurrence settings', () { + final event = CalendarEventData( + title: 'Daily Standup', + startDate: now, + startTime: TimeOfDay(hour: 9, minute: 0), + endTime: TimeOfDay(hour: 9, minute: 30), + recurrenceSettings: RecurrenceSettings( + startDate: now, + frequency: RepeatFrequency.daily, + ), + ); + + expect(event.isRecurringEvent, true); + }); + + test('occursOnDate should return true for event date', () { + final event = CalendarEventData( + title: 'Event', + startDate: now, + ); + + expect(event.occursOnDate(now), true); + }); + + test('occursOnDate should return true for dates in multi-day range', () { + final startDate = DateTime(2024, 1, 10); + final endDate = DateTime(2024, 1, 15); + + final event = CalendarEventData.multiDay( + title: 'Conference', + startDate: startDate, + endDate: endDate, + ); + + expect(event.occursOnDate(DateTime(2024, 1, 10)), true); + expect(event.occursOnDate(DateTime(2024, 1, 12)), true); + expect(event.occursOnDate(DateTime(2024, 1, 15)), true); + expect(event.occursOnDate(DateTime(2024, 1, 9)), false); + expect(event.occursOnDate(DateTime(2024, 1, 16)), false); + }); + }); + + group('CalendarEventData.copyWith Tests', () { + test('should create copy with updated title', () { + final original = CalendarEventData.timeRanged( + title: 'Original', + date: now, + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 12, minute: 0), + ); + + final copy = original.copyWith(title: 'Updated'); + + expect(copy.title, 'Updated'); + expect(copy.date, original.date); + expect(copy.startTime, original.startTime); + }); + + test('should create copy with updated times', () { + final original = CalendarEventData.timeRanged( + title: 'Event', + date: now, + startTime: TimeOfDay(hour: 10, minute: 0), + endTime: TimeOfDay(hour: 12, minute: 0), + ); + + final copy = original.copyWith( + startTime: TimeOfDay(hour: 14, minute: 0), + endTime: TimeOfDay(hour: 16, minute: 0), + ); + + expect(copy.startTime, TimeOfDay(hour: 14, minute: 0)); + expect(copy.endTime, TimeOfDay(hour: 16, minute: 0)); + expect(copy.title, original.title); + }); + + test('should create copy with recurrence settings', () { + final original = CalendarEventData( + title: 'Event', + startDate: now, + ); + + final recurrence = RecurrenceSettings( + startDate: now, + frequency: RepeatFrequency.weekly, + ); + + final copy = original.copyWith(recurrenceSettings: recurrence); + + expect(copy.recurrenceSettings, recurrence); + expect(copy.isRecurringEvent, true); + }); + }); + + group('CalendarEventData Recurring Event Tests', () { + test('should create daily recurring event', () { + final event = CalendarEventData.timeRanged( + title: 'Daily Standup', + date: now, + startTime: TimeOfDay(hour: 9, minute: 0), + endTime: TimeOfDay(hour: 9, minute: 30), + recurrenceSettings: RecurrenceSettings( + startDate: now, + frequency: RepeatFrequency.daily, + recurrenceEndOn: RecurrenceEnd.after, + occurrences: 5, + ), + ); + + expect(event.isRecurringEvent, true); + expect(event.recurrenceSettings!.frequency, RepeatFrequency.daily); + expect(event.recurrenceSettings!.occurrences, 5); + }); + + test('should create weekly recurring event with specific weekdays', () { + final event = CalendarEventData.timeRanged( + title: 'Team Meeting', + date: now, + startTime: TimeOfDay(hour: 14, minute: 0), + endTime: TimeOfDay(hour: 15, minute: 0), + recurrenceSettings: RecurrenceSettings( + startDate: now, + frequency: RepeatFrequency.weekly, + weekdays: [WeekDays.monday, WeekDays.wednesday, WeekDays.friday], + ), + ); + + expect(event.isRecurringEvent, true); + expect(event.recurrenceSettings!.weekdays.length, 3); + expect( + event.recurrenceSettings!.weekdays, + contains(WeekDays.monday), + ); + }); + + test('should create monthly recurring event', () { + final event = CalendarEventData.wholeDay( + title: 'Monthly Review', + date: DateTime(2024, 1, 15), + recurrenceSettings: RecurrenceSettings( + startDate: DateTime(2024, 1, 15), + frequency: RepeatFrequency.monthly, + endDate: DateTime(2024, 12, 15), + ), + ); + + expect(event.isRecurringEvent, true); + expect(event.recurrenceSettings!.frequency, RepeatFrequency.monthly); + }); + }); } diff --git a/test/extensions_test.dart b/test/extensions_test.dart index 61f23cc4..b0b79c6a 100644 --- a/test/extensions_test.dart +++ b/test/extensions_test.dart @@ -90,7 +90,7 @@ void testAllFirstDayOfTheWeek(DateTime date) { } void testFirstDayOfTheWeekForAllWeekDays(DateTime firstDayOfTheWeek) { - final weekDays = getWeekDays(firstDayOfTheWeek); + final weekDays = firstDayOfTheWeek.weekDayEnum; for (var i = 0; i < 7; i++) { final date = DateTime( firstDayOfTheWeek.year, @@ -119,7 +119,7 @@ void testAllLastDayOfTheWeek(DateTime date) { } void testLastDayOfTheWeekForAllWeekDays(DateTime lastDayOfTheWeek) { - final weekDays = nextWeekDays(getWeekDays(lastDayOfTheWeek)); + final weekDays = nextWeekDays(lastDayOfTheWeek.weekDayEnum); for (var i = 0; i < 7; i++) { final date = DateTime( lastDayOfTheWeek.year, @@ -138,27 +138,7 @@ DateTime getWeekStartDay(DateTime date, WeekDays start) { } WeekDays getWeekDays(DateTime date) { - switch (date.weekday) { - case 1: - return WeekDays.monday; - case 2: - return WeekDays.tuesday; - case 3: - return WeekDays.wednesday; - case 4: - return WeekDays.thursday; - case 5: - return WeekDays.friday; - case 6: - return WeekDays.saturday; - case 7: - return WeekDays.sunday; - default: - throw Exception(""" - Date $date has unrecognisable weekday expencted [1-7], - but ${date.weekday} was provided. - """); - } + return date.weekDayEnum; } WeekDays nextWeekDays(WeekDays current) { diff --git a/test/src/event_controller_test.dart b/test/src/event_controller_test.dart index 91ce5471..565096f8 100644 --- a/test/src/event_controller_test.dart +++ b/test/src/event_controller_test.dart @@ -1,4 +1,5 @@ import 'package:calendar_view/calendar_view.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -8,12 +9,12 @@ void main() { final now = DateTime.now(); controller.add(CalendarEventData( title: 'none', - date: now, - startTime: now, - endTime: now.add(Duration(hours: 1)))); + startDate: now, + startTime: TimeOfDay(hour: now.hour, minute: now.minute), + endTime: TimeOfDay(hour: (now.hour + 1) % 24, minute: now.minute))); controller.add(CalendarEventData( title: 'All Day', - date: DateTime.now().withoutTime, + startDate: now.withoutTime, )); expect(controller.getFullDayEvent(now).length, equals(1));