diff --git a/.github/workflows/github_pages.yml b/.github/workflows/github_pages.yml index 24abb14e..fc14e618 100644 --- a/.github/workflows/github_pages.yml +++ b/.github/workflows/github_pages.yml @@ -19,6 +19,8 @@ jobs: - name: Install Flutter uses: britannio/action-install-flutter@v1.1 + with: + version: 3.24.3 - name: Install dependencies run: flutter pub get diff --git a/CHANGELOG.md b/CHANGELOG.md index 836fa1f6..8a81a19d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Fixes issue in showing quarter hours when startHour is provided. [#387](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/387) - Use `hourLinePainter` in `DayView` [#386](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/386) +- Refactor `SideEventArranger` to arrange events properly. [#290](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/290) # [1.2.0 - 10 May 2024](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/tree/1.2.0) diff --git a/example/web/index.html b/example/web/index.html index 2bc631d9..3003eac9 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -33,13 +33,7 @@ - + diff --git a/lib/src/calendar_event_data.dart b/lib/src/calendar_event_data.dart index 80256dff..e6b053fd 100644 --- a/lib/src/calendar_event_data.dart +++ b/lib/src/calendar_event_data.dart @@ -15,7 +15,7 @@ class CalendarEventData { /// 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] + /// This is required when you are using [CalendarEventData] for [DayView] or [WeekView] final DateTime? startTime; /// Defines the end time of the event. @@ -81,6 +81,24 @@ class CalendarEventData { (startTime!.isDayStart && endTime!.isDayStart)); } + 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 (end.isDayStart) { + final difference = + end.add(Duration(days: 1) - Duration(seconds: 1)).difference(start); + + return difference + Duration(seconds: 1); + } else { + return end.difference(start); + } + } + /// Returns a boolean that defines whether current event is occurring on /// [currentDate] or not. /// diff --git a/lib/src/event_arrangers/merge_event_arranger.dart b/lib/src/event_arrangers/merge_event_arranger.dart index 0e63913c..4e7c8c47 100644 --- a/lib/src/event_arrangers/merge_event_arranger.dart +++ b/lib/src/event_arrangers/merge_event_arranger.dart @@ -40,10 +40,13 @@ class MergeEventArranger extends EventArranger { //Checking if startTime and endTime are correct for (final event in events) { + if (event.startTime == null || event.endTime == null) { + debugLog('startTime or endTime is null for ${event.title}'); + continue; + } + // Checks if an event has valid start and end time. - if (event.startTime == null || - event.endTime == null || - event.endTime!.getTotalMinutes <= event.startTime!.getTotalMinutes) { + if (event.endTime!.getTotalMinutes <= event.startTime!.getTotalMinutes) { if (!(event.endTime!.getTotalMinutes == 0 && event.startTime!.getTotalMinutes > 0)) { assert(() { diff --git a/lib/src/event_arrangers/side_event_arranger.dart b/lib/src/event_arrangers/side_event_arranger.dart index 1f5e49e9..c11a725b 100644 --- a/lib/src/event_arrangers/side_event_arranger.dart +++ b/lib/src/event_arrangers/side_event_arranger.dart @@ -23,6 +23,7 @@ class SideEventArranger extends EventArranger { /// /// Make sure that all the events that are passed in [events], must be in /// ascending order of start time. + @override List> arrange({ required List> events, @@ -31,113 +32,211 @@ class SideEventArranger extends EventArranger { required double heightPerMinute, required int startHour, }) { - final mergedEvents = MergeEventArranger( - includeEdges: includeEdges, - ).arrange( - events: events, - height: height, - width: width, - heightPerMinute: heightPerMinute, - startHour: startHour, - ); + final totalWidth = width; + + List<_SideEventConfigs> _categorizedColumnedEvents( + List> events) { + final merged = MergeEventArranger(includeEdges: includeEdges).arrange( + events: events, + height: height, + width: width, + heightPerMinute: heightPerMinute, + startHour: startHour, + ); + + final arranged = <_SideEventConfigs>[]; + + for (final event in merged) { + if (event.events.isEmpty) { + // NOTE(parth): This is safety condition. + // This condition should never be true. + // If by chance this becomes true, there is something wrong with + // logic. And that need to be fixed ASAP. + + continue; + } + + if (event.events.length > 1) { + // NOTE: This means all the events are overlapping with each other. + // So, we will extract all the events that can be fit in + // Single column without overlapping and run the function + // again for the rest of the events. + + final columnedEvents = _extractSingleColumnEvents( + event.events, + event.endDuration.getTotalMinutes, + ); - final arrangedEvents = >[]; + final sided = _categorizedColumnedEvents( + event.events.where((e) => !columnedEvents.contains(e)).toList(), + ); - for (final event in mergedEvents) { - // If there is only one event in list that means, there - // is no simultaneous events. - if (event.events.length == 1) { - arrangedEvents.add(event); - continue; + var maxColumns = 1; + + for (final event in sided) { + if (event.columns > maxColumns) { + maxColumns = event.columns; + } + } + + arranged.add(_SideEventConfigs( + columns: maxColumns + 1, + event: columnedEvents, + sideEvents: sided, + )); + } else { + // If this block gets executed that means we have only one event. + // Return the event as is. + + arranged.add(_SideEventConfigs(columns: 1, event: event.events)); + } } - final concurrentEvents = event.events; + return arranged; + } - if (concurrentEvents.isEmpty) continue; + List> _arrangeEvents( + List<_SideEventConfigs> events, double width, double offset) { + final arranged = >[]; - var column = 1; - final sideEventData = <_SideEventData>[]; - var currentEventIndex = 0; + for (final event in events) { + final slotWidth = width / event.columns; - while (concurrentEvents.isNotEmpty) { - final event = concurrentEvents[currentEventIndex]; - final end = event.endTime!.getTotalMinutes == 0 - ? Constants.minutesADay - : event.endTime!.getTotalMinutes; - sideEventData.add(_SideEventData(column: column, event: event)); - concurrentEvents.removeAt(currentEventIndex); + if (event.event.isNotEmpty) { + // TODO(parth): Arrange events and add it in arranged. - while (currentEventIndex < concurrentEvents.length) { - if (end < - concurrentEvents[currentEventIndex].startTime!.getTotalMinutes) { - break; - } + arranged.addAll(event.event.map((e) { + final startTime = e.startTime!; + final endTime = e.endTime!; - currentEventIndex++; + // startTime.getTotalMinutes returns the number of minutes from 00h00 to the beginning hour of the event + // But the first hour to be displayed (startHour) could be 06h00, so we have to substract + // The number of minutes from 00h00 to startHour which is equal to startHour * 60 + + final bottom = height - + (endTime.getTotalMinutes - (startHour * 60) == 0 + ? Constants.minutesADay - (startHour * 60) + : endTime.getTotalMinutes - (startHour * 60)) * + heightPerMinute; + + final top = (startTime.getTotalMinutes - (startHour * 60)) * + heightPerMinute; + + return OrganizedCalendarEventData( + left: offset, + right: totalWidth - (offset + slotWidth), + top: top, + bottom: bottom, + startDuration: startTime, + endDuration: endTime, + events: [e], + ); + })); } - if (concurrentEvents.isNotEmpty && - currentEventIndex >= concurrentEvents.length) { - column++; - currentEventIndex = 0; + if (event.sideEvents.isNotEmpty) { + arranged.addAll(_arrangeEvents( + event.sideEvents, + math.max(0, width - slotWidth), + slotWidth + offset, + )); } } - final slotWidth = width / column; + return arranged; + } - for (final sideEvent in sideEventData) { - if (sideEvent.event.startTime == null || - sideEvent.event.endTime == null) { - assert(() { - try { - debugPrint("Start time or end time of an event can not be null. " - "This ${sideEvent.event} will be ignored."); - } catch (e) {} // ignore:empty_catches + // By default the offset will be 0. - return true; - }(), "Can not add event in the list."); + final columned = _categorizedColumnedEvents(events); + final arranged = _arrangeEvents(columned, totalWidth, 0); + return arranged; + } - continue; + List> _extractSingleColumnEvents( + List> events, int end) { + // Find the longest event from the list. + final longestEvent = events.fold>( + events.first, + (e1, e2) => e1.duration > e2.duration ? e1 : e2, + ); + + // Create a new list from events and remove the longest one from it. + final searchEvents = [...events]..remove(longestEvent); + + // Create a new list for events in single column. + // Right now it has longest event, + // By the end of the function, this will have the list of the events, + // that are not intersecting with each other. + // and this will be returned from the function. + final columnedEvents = [longestEvent]; + + // Calculate effective end minute from latest columned event. + var endMinutes = longestEvent.endTime!.getTotalMinutes; + + // Run the loop while effective end minute of columned events are + // less than end. + while (endMinutes < end && searchEvents.isNotEmpty) { + // Maps the event with it's duration. + final mappings = >{}; + + // Create a new list from searchEvents. + for (final event in [...searchEvents]) { + // Need to add logic to include edges... + final start = event.startTime!.getTotalMinutes; + + // TODO(parth): Need to improve this. + // This does not handle the case where there is a event before the + // longest event which is not intersecting. + // + if (start < endMinutes || (includeEdges && start == endMinutes)) { + // Remove search event from list so, we do not iterate through it + // again. + searchEvents.remove(event); + } else { + // Add the event in mappings. + final diff = event.startTime!.getTotalMinutes - endMinutes; + + mappings.addAll({ + diff: event, + }); } + } - final startTime = sideEvent.event.startTime!; - final endTime = sideEvent.event.endTime!; + // This can be any integer larger than 1440 as one day has 1440 minutes. + // so, different of 2 events end and start time will never be greater than + // 1440. + var min = 4000; - // startTime.getTotalMinutes returns the number of minutes from 00h00 to the beginning hour of the event - // But the first hour to be displayed (startHour) could be 06h00, so we have to substract - // The number of minutes from 00h00 to startHour which is equal to startHour * 60 + for (final mapping in mappings.entries) { + if (mapping.key < min) { + min = mapping.key; + } + } - final bottom = height - - (endTime.getTotalMinutes - (startHour * 60) == 0 - ? Constants.minutesADay - (startHour * 60) - : endTime.getTotalMinutes - (startHour * 60)) * - heightPerMinute; + if (mappings[min] != null) { + // If mapping had min event, add it in columnedEvents, + // and remove it from searchEvents so, we do not iterate through it + // again. + columnedEvents.add(mappings[min]!); + searchEvents.remove(mappings[min]); - final top = - (startTime.getTotalMinutes - (startHour * 60)) * heightPerMinute; - - arrangedEvents.add(OrganizedCalendarEventData( - left: slotWidth * (sideEvent.column - 1), - right: slotWidth * (column - sideEvent.column), - top: top, - bottom: bottom, - startDuration: startTime, - endDuration: endTime, - events: [sideEvent.event], - )); + endMinutes = mappings[min]!.endTime!.getTotalMinutes; } } - return arrangedEvents; + return columnedEvents; } } -class _SideEventData { - final int column; - final CalendarEventData event; +class _SideEventConfigs { + final int columns; + final List> event; + final List<_SideEventConfigs> sideEvents; - const _SideEventData({ - required this.column, - required this.event, + const _SideEventConfigs({ + this.event = const [], + required this.columns, + this.sideEvents = const [], }); } diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index 10c60a44..36077fd0 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -126,7 +126,7 @@ extension DateTimeExtensions on DateTime { other.microsecond == microsecond; } - bool get isDayStart => hour % 24 == 0 && minute % 60 == 0; + bool get isDayStart => hour == 0 && minute == 0; @Deprecated( "This extension is not being used in this package and will be removed " @@ -210,3 +210,13 @@ extension IntExtension on int { return toString().padLeft(2, '0'); } } + +void debugLog(String message) { + assert(() { + try { + debugPrint(message); + } catch (e) {} //ignore: empty_catches Suppress exception... + + return false; + }(), ''); +}