Skip to content

Commit

Permalink
🐛 Fixes issue #290.
Browse files Browse the repository at this point in the history
  • Loading branch information
ParthBaraiya committed Sep 17, 2024
1 parent 126ecd2 commit 5616aa5
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 91 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/github_pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:

- name: Install Flutter
uses: britannio/[email protected]
with:
version: 3.24.3

- name: Install dependencies
run: flutter pub get
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 1 addition & 7 deletions example/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,7 @@
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('flutter-first-frame', function () {
navigator.serviceWorker.register('flutter_service_worker.js');
});
}
</script>
<script src="flutter_bootstrap.js" async></script>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
20 changes: 19 additions & 1 deletion lib/src/calendar_event_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class CalendarEventData<T extends Object?> {

/// 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.
Expand Down Expand Up @@ -81,6 +81,24 @@ class CalendarEventData<T extends Object?> {
(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.
///
Expand Down
9 changes: 6 additions & 3 deletions lib/src/event_arrangers/merge_event_arranger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@ class MergeEventArranger<T extends Object?> extends EventArranger<T> {

//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(() {
Expand Down
263 changes: 184 additions & 79 deletions lib/src/event_arrangers/side_event_arranger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class SideEventArranger<T extends Object?> extends EventArranger<T> {
///
/// Make sure that all the events that are passed in [events], must be in
/// ascending order of start time.
@override
List<OrganizedCalendarEventData<T>> arrange({
required List<CalendarEventData<T>> events,
Expand All @@ -31,113 +32,217 @@ class SideEventArranger<T extends Object?> extends EventArranger<T> {
required double heightPerMinute,
required int startHour,
}) {
final mergedEvents = MergeEventArranger<T>(
includeEdges: includeEdges,
).arrange(
events: events,
height: height,
width: width,
heightPerMinute: heightPerMinute,
startHour: startHour,
);
final totalWidth = width;

List<_SideEventConfigs<T>> _categorizedColumnedEvents(
List<CalendarEventData<T>> events) {
final merged = MergeEventArranger<T>(includeEdges: includeEdges).arrange(
events: events,
height: height,
width: width,
heightPerMinute: heightPerMinute,
startHour: startHour,
);

final arranged = <_SideEventConfigs<T>>[];

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 = <OrganizedCalendarEventData<T>>[];
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<OrganizedCalendarEventData<T>> _arrangeEvents(
List<_SideEventConfigs<T>> events, double width, double offset) {
final arranged = <OrganizedCalendarEventData<T>>[];

var column = 1;
final sideEventData = <_SideEventData<T>>[];
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;

final left = offset;
final right = totalWidth - (offset + slotWidth);

print(
"Arrange Event: Top: $top, Left: $left, Right: $right, Bottom: $bottom");

return OrganizedCalendarEventData<T>(
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<CalendarEventData<T>> _extractSingleColumnEvents(
List<CalendarEventData<T>> events, int end) {
// Find the longest event from the list.
final longestEvent = events.fold<CalendarEventData<T>>(
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 = <int, CalendarEventData<T>>{};

// 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<T>(
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<T> {
final int column;
final CalendarEventData<T> event;
class _SideEventConfigs<T extends Object?> {
final int columns;
final List<CalendarEventData<T>> event;
final List<_SideEventConfigs<T>> sideEvents;

const _SideEventData({
required this.column,
required this.event,
const _SideEventConfigs({
this.event = const [],
required this.columns,
this.sideEvents = const [],
});
}
Loading

0 comments on commit 5616aa5

Please sign in to comment.