Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Fixes issue #290. #405

Merged
merged 1 commit into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
257 changes: 178 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,211 @@ 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;

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
Loading