Proper handling of DateTimeOffset, support for Multi-Day events and reacting to adding/removing events for the days.

This commit is contained in:
Burak Kaan Köse
2024-12-30 23:10:51 +01:00
parent 8cc7d46d7b
commit 8fd09bcad4
23 changed files with 340 additions and 234 deletions

View File

@@ -258,13 +258,6 @@ namespace Wino.Calendar.ViewModels
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly ICalendarService _calendarService; private readonly ICalendarService _calendarService;
//public override void OnPageLoaded()
//{
// base.OnPageLoaded();
// TodayClicked();
//}
#region Commands #region Commands
[RelayCommand] [RelayCommand]
@@ -341,7 +334,5 @@ namespace Wino.Calendar.ViewModels
=> await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled); => await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled);
public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItemIndex = 1; public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItemIndex = 1;
//public void Receive(GoToCalendarDayMessage message) => SelectedMenuItemIndex = -1;
} }
} }

View File

@@ -7,10 +7,12 @@ using System.Threading.Tasks;
using CommunityToolkit.Diagnostics; using CommunityToolkit.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using MoreLinq;
using Serilog; using Serilog;
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces; using Wino.Calendar.ViewModels.Interfaces;
using Wino.Core.Domain.Collections; using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
@@ -70,20 +72,16 @@ namespace Wino.Calendar.ViewModels
} }
private void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e) private void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
{ => FilterActiveCalendars(DayRanges);
// For all date ranges, update the events.
foreach (var dayRange in DayRanges)
{
_ = InitializeCalendarEventsForDayRangeAsync(dayRange);
}
}
private void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e) private void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
=> FilterActiveCalendars(DayRanges);
private void FilterActiveCalendars(IEnumerable<DayRangeRenderModel> dayRangeRenderModels)
{ {
foreach (var range in DayRanges) var days = dayRangeRenderModels.SelectMany(a => a.CalendarDays);
{
_ = InitializeCalendarEventsForDayRangeAsync(range); days.ForEach(a => a.EventsCollection.FilterByCalendars(_accountCalendarStateService.ActiveCalendars.Select(a => a.Id)));
}
} }
// TODO: Replace when calendar settings are updated. // TODO: Replace when calendar settings are updated.
@@ -297,6 +295,9 @@ namespace Wino.Calendar.ViewModels
await InitializeCalendarEventsForDayRangeAsync(renderModel).ConfigureAwait(false); await InitializeCalendarEventsForDayRangeAsync(renderModel).ConfigureAwait(false);
} }
// Filter by active calendars. This is a quick operation, and things are not on the UI yet.
FilterActiveCalendars(renderModels);
CalendarLoadDirection animationDirection = calendarLoadDirection; CalendarLoadDirection animationDirection = calendarLoadDirection;
bool removeCurrent = calendarLoadDirection == CalendarLoadDirection.Replace; bool removeCurrent = calendarLoadDirection == CalendarLoadDirection.Replace;
@@ -372,22 +373,39 @@ namespace Wino.Calendar.ViewModels
} }
// TODO... // TODO...
private async void EventsUpdatedInDayHeader(object sender, CalendarDayModel e) private void EventsUpdatedInDayHeader(object sender, CalendarDayModel e)
{ {
// Find the day range model that contains the day model.
// TODO: Maybe optimize by just updating the day?
if (sender is DayRangeRenderModel dayRangeRenderModel) }
{
await InitializeCalendarEventsForDayRangeAsync(dayRangeRenderModel);
}
protected override async void OnCalendarEventAdded(CalendarItem calendarItem)
{
base.OnCalendarEventAdded(calendarItem);
//var dayRange = DayRanges.FirstOrDefault(a => a.CalendarDays.Contains(e)); // test
var calendar = await _calendarService.GetAccountCalendarAsync(Guid.Parse("9ead7613-dacb-4163-8d33-2e32e65008a1"));
//if (dayRange == null) return; calendarItem.AssignedCalendar = calendar;
// Check if event falls into the current date range.
//await InitializeCalendarEventsForDayRangeAsync(dayRange); var loadedDateRange = GetLoadedDateRange();
if (loadedDateRange == null) return;
// Check whether this event falls into any of the loaded date ranges.
//if (calendarItem.Period.Start >= loadedDateRange.StartDate && calendarItem.Period.Start.Date <= loadedDateRange.EndDate)
//{
// // Find the day representation for the event.
// var dayModel = DayRanges.SelectMany(a => a.CalendarDays).FirstOrDefault(a => a.RepresentingDate.Date == calendarItem.Period.Start.Date);
// if (dayModel == null) return;
// var calendarItemViewModel = new CalendarItemViewModel(calendarItem);
// await ExecuteUIThread(() =>
// {
// dayModel.EventsCollection.AddCalendarItem(calendarItemViewModel);
// });
//}
} }
private async Task InitializeCalendarEventsForDayRangeAsync(DayRangeRenderModel dayRangeRenderModel) private async Task InitializeCalendarEventsForDayRangeAsync(DayRangeRenderModel dayRangeRenderModel)
@@ -401,38 +419,32 @@ namespace Wino.Calendar.ViewModels
}); });
} }
// Load for each selected calendar from the state. // Initialization is done for all calendars, regardless whether they are actively selected or not.
var checkedCalendarViewModels = _accountCalendarStateService.GroupedAccountCalendars // This is because the filtering is cached internally of the calendar items in CalendarEventCollection.
.SelectMany(a => a.AccountCalendars) var allCalendars = _accountCalendarStateService.GroupedAccountCalendars.SelectMany(a => a.AccountCalendars);
.Where(b => b.IsChecked);
foreach (var calendarViewModel in checkedCalendarViewModels) foreach (var calendarViewModel in allCalendars)
{ {
// Check all the events for the given date range and calendar. // Check all the events for the given date range and calendar.
// Then find the day representation for all the events returned, and add to the collection. // Then find the day representation for all the events returned, and add to the collection.
var events = await _calendarService.GetCalendarEventsAsync(calendarViewModel, var events = await _calendarService.GetCalendarEventsAsync(calendarViewModel, dayRangeRenderModel).ConfigureAwait(false);
dayRangeRenderModel.Period.Start,
dayRangeRenderModel.Period.End)
.ConfigureAwait(false);
var groupedEvents = events.GroupBy(a => a.StartTime.Date); foreach (var @event in events)
foreach (var group in groupedEvents)
{ {
var startDate = group.Key; // Find the days that the event falls into.
// TODO: Multi-day events are not fully supported yet.
var calendarDayModel = dayRangeRenderModel.CalendarDays.FirstOrDefault(a => a.RepresentingDate.Date == startDate); var allDaysForEvent = dayRangeRenderModel.CalendarDays.Where(a => a.Period.OverlapsWith(@event.Period));
if (calendarDayModel == null) continue; foreach (var calendarDay in allDaysForEvent)
var calendarItemViewModels = group.Select(a => new CalendarItemViewModel(a));
await ExecuteUIThread(() =>
{ {
// Use range-based add for performance. var calendarItemViewModel = new CalendarItemViewModel(@event);
calendarDayModel.EventsCollection.AddCalendarItemRange(calendarItemViewModels); await ExecuteUIThread(() =>
}); {
calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel);
});
}
} }
} }
} }
@@ -532,7 +544,7 @@ namespace Wino.Calendar.ViewModels
// TODO: This might need throttling due to slider in the settings page for hour height. // TODO: This might need throttling due to slider in the settings page for hour height.
// or make sure the slider does not update on each tick but on focus lost. // or make sure the slider does not update on each tick but on focus lost.
Messenger.Send(new LoadCalendarMessage(DateTime.UtcNow.Date, CalendarInitInitiative.App, true)); // Messenger.Send(new LoadCalendarMessage(DateTime.UtcNow.Date, CalendarInitInitiative.App, true));
} }
} }
} }

View File

@@ -14,13 +14,15 @@ namespace Wino.Calendar.ViewModels.Data
public Guid Id => CalendarItem.Id; public Guid Id => CalendarItem.Id;
public DateTimeOffset StartTime => CalendarItem.StartTime; public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar;
public int DurationInMinutes => CalendarItem.DurationInMinutes; public DateTime StartDate { get => CalendarItem.StartDate; set => CalendarItem.StartDate = value; }
public TimeRange Period => CalendarItem.Period; public DateTime EndDate => CalendarItem.EndDate;
public IAccountCalendar AssignedCalendar => ((ICalendarItem)CalendarItem).AssignedCalendar; public double DurationInSeconds { get => CalendarItem.DurationInSeconds; set => CalendarItem.DurationInSeconds = value; }
public ITimePeriod Period => CalendarItem.Period;
public CalendarItemViewModel(CalendarItem calendarItem) public CalendarItemViewModel(CalendarItem calendarItem)
{ {

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
@@ -17,5 +18,10 @@ namespace Wino.Calendar.ViewModels.Interfaces
public void AddAccountCalendar(AccountCalendarViewModel accountCalendar); public void AddAccountCalendar(AccountCalendarViewModel accountCalendar);
public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar); public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar);
/// <summary>
/// Enumeration of currently selected calendars.
/// </summary>
IEnumerable<AccountCalendarViewModel> ActiveCalendars { get; }
} }
} }

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic; using Windows.UI.Xaml;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Collections; using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
@@ -76,9 +75,6 @@ namespace Wino.Calendar.Controls
collection.CalendarItemAdded += SingleEventUpdated; collection.CalendarItemAdded += SingleEventUpdated;
collection.CalendarItemRemoved += SingleEventUpdated; collection.CalendarItemRemoved += SingleEventUpdated;
collection.CalendarItemRangeAdded += CollectionOfEventsUpdated;
collection.CalendarItemRangeRemoved += CollectionOfEventsUpdated;
collection.CalendarItemsCleared += EventsCleared; collection.CalendarItemsCleared += EventsCleared;
} }
@@ -87,14 +83,10 @@ namespace Wino.Calendar.Controls
collection.CalendarItemAdded -= SingleEventUpdated; collection.CalendarItemAdded -= SingleEventUpdated;
collection.CalendarItemRemoved -= SingleEventUpdated; collection.CalendarItemRemoved -= SingleEventUpdated;
collection.CalendarItemRangeAdded -= CollectionOfEventsUpdated;
collection.CalendarItemRangeRemoved -= CollectionOfEventsUpdated;
collection.CalendarItemsCleared -= EventsCleared; collection.CalendarItemsCleared -= EventsCleared;
} }
private void SingleEventUpdated(object sender, ICalendarItem calendarItem) => UpdateCollectionVisuals(); private void SingleEventUpdated(object sender, ICalendarItem calendarItem) => UpdateCollectionVisuals();
private void CollectionOfEventsUpdated(object sender, List<ICalendarItem> calendarItems) => UpdateCollectionVisuals();
private void EventsCleared(object sender, System.EventArgs e) => UpdateCollectionVisuals(); private void EventsCleared(object sender, System.EventArgs e) => UpdateCollectionVisuals();
private void UpdateCollectionVisuals() private void UpdateCollectionVisuals()

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
using Wino.Calendar.Args; using Wino.Calendar.Args;
@@ -95,8 +94,6 @@ namespace Wino.Calendar.Controls
{ {
if (canvas == null) return; if (canvas == null) return;
Debug.WriteLine("Deregister active canvas.");
canvas.SelectedDateTime = null; canvas.SelectedDateTime = null;
canvas.TimelineCellSelected -= ActiveTimelineCellSelected; canvas.TimelineCellSelected -= ActiveTimelineCellSelected;
canvas.TimelineCellUnselected -= ActiveTimelineCellUnselected; canvas.TimelineCellUnselected -= ActiveTimelineCellUnselected;
@@ -106,8 +103,6 @@ namespace Wino.Calendar.Controls
{ {
if (canvas == null) return; if (canvas == null) return;
Debug.WriteLine("Register new canvas.");
canvas.SelectedDateTime = null; canvas.SelectedDateTime = null;
canvas.TimelineCellSelected += ActiveTimelineCellSelected; canvas.TimelineCellSelected += ActiveTimelineCellSelected;
canvas.TimelineCellUnselected += ActiveTimelineCellUnselected; canvas.TimelineCellUnselected += ActiveTimelineCellUnselected;
@@ -127,13 +122,6 @@ namespace Wino.Calendar.Controls
InternalFlipView = GetTemplateChild(PART_WinoFlipView) as WinoCalendarFlipView; InternalFlipView = GetTemplateChild(PART_WinoFlipView) as WinoCalendarFlipView;
} }
private void FlipViewsActiveTimelineCanvasChanged(object sender, WinoDayTimelineCanvas e)
{
ActiveCanvas = e;
SelectedFlipViewDayRange = InternalFlipView.SelectedItem as DayRangeRenderModel;
}
private void ActiveTimelineCellUnselected(object sender, TimelineCellUnselectedArgs e) private void ActiveTimelineCellUnselected(object sender, TimelineCellUnselectedArgs e)
=> TimelineCellUnselected?.Invoke(this, e); => TimelineCellUnselected?.Invoke(this, e);

View File

@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using Itenso.TimePeriod;
using Windows.Foundation; using Windows.Foundation;
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
@@ -21,6 +22,14 @@ namespace Wino.Calendar.Controls
public static readonly DependencyProperty EventItemMarginProperty = DependencyProperty.Register(nameof(EventItemMargin), typeof(Thickness), typeof(WinoCalendarPanel), new PropertyMetadata(new Thickness(0, 0, 0, 0))); public static readonly DependencyProperty EventItemMarginProperty = DependencyProperty.Register(nameof(EventItemMargin), typeof(Thickness), typeof(WinoCalendarPanel), new PropertyMetadata(new Thickness(0, 0, 0, 0)));
public static readonly DependencyProperty HourHeightProperty = DependencyProperty.Register(nameof(HourHeight), typeof(double), typeof(WinoCalendarPanel), new PropertyMetadata(0d)); public static readonly DependencyProperty HourHeightProperty = DependencyProperty.Register(nameof(HourHeight), typeof(double), typeof(WinoCalendarPanel), new PropertyMetadata(0d));
public static readonly DependencyProperty PeriodProperty = DependencyProperty.Register(nameof(Period), typeof(ITimePeriod), typeof(WinoCalendarPanel), new PropertyMetadata(null));
public ITimePeriod Period
{
get { return (ITimePeriod)GetValue(PeriodProperty); }
set { SetValue(PeriodProperty, value); }
}
public double HourHeight public double HourHeight
{ {
@@ -41,11 +50,18 @@ namespace Wino.Calendar.Controls
private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight) private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight)
{ {
var childStart = calendarItemViewModel.StartTime; var childStart = calendarItemViewModel.StartDate;
double totalMinutes = 1440; if (childStart <= Period.Start)
double minutesFromStart = (childStart - childStart.DateTime.Date).TotalMinutes; {
return (minutesFromStart / totalMinutes) * availableHeight; // Event started before or exactly at the periods tart. This might be a multi-day event.
// We can simply consider event must not have a top margin.
return 0d;
}
double minutesFromStart = (childStart - Period.Start).TotalMinutes;
return (minutesFromStart / 1440) * availableHeight;
} }
private double GetChildWidth(CalendarItemMeasurement calendarItemMeasurement, double availableWidth) private double GetChildWidth(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
@@ -56,16 +72,48 @@ namespace Wino.Calendar.Controls
private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth) private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
=> availableWidth * calendarItemMeasurement.Left; => availableWidth * calendarItemMeasurement.Left;
private double GetChildHeight(DateTimeOffset childStart, DateTimeOffset childEnd) private double GetChildHeight(ICalendarItem child)
{ {
double totalMinutes = 1440; double childDurationInMinutes = 0d;
double availableHeight = HourHeight * 24; double availableHeight = HourHeight * 24;
double childDuration = (childEnd - childStart).TotalMinutes;
return (childDuration / totalMinutes) * availableHeight; var childStart = child.Period.Start;
var childEnd = child.Period.End;
// Multi-day event.
if (childStart < Period.Start)
{
if (childEnd >= Period.End)
{
// Event spans the whole period.
return availableHeight;
}
else
{
// Check how many of the event falls into the current period.
childDurationInMinutes = (childEnd - Period.Start).TotalMinutes;
}
}
else
{
childDurationInMinutes = (childEnd - childStart).TotalMinutes;
}
return (childDurationInMinutes / 1440) * availableHeight;
}
protected override Size MeasureOverride(Size availableSize)
{
ResetMeasurements();
return base.MeasureOverride(availableSize);
} }
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
if (Period == null || HourHeight == 0d) return finalSize;
// Measure/arrange each child height and width. // Measure/arrange each child height and width.
// This is a vertical calendar. Therefore the height of each child is the duration of the event. // This is a vertical calendar. Therefore the height of each child is the duration of the event.
// Children weights for left and right will be saved if they don't exist. // Children weights for left and right will be saved if they don't exist.
@@ -99,7 +147,7 @@ namespace Wino.Calendar.Controls
var childMeasurement = _measurements[child.Id]; var childMeasurement = _measurements[child.Id];
double childHeight = Math.Max(0, GetChildHeight(child.StartTime, child.StartTime.AddMinutes(child.DurationInMinutes))); double childHeight = Math.Max(0, GetChildHeight(child));
double childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width)); double childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width));
double childTop = Math.Max(0, GetChildTopMargin(child, availableHeight)); double childTop = Math.Max(0, GetChildTopMargin(child, availableHeight));
double childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth)); double childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth));
@@ -142,7 +190,7 @@ namespace Wino.Calendar.Controls
var columns = new List<List<ICalendarItem>>(); var columns = new List<List<ICalendarItem>>();
DateTime? lastEventEnding = null; DateTime? lastEventEnding = null;
foreach (var ev in events.OrderBy(ev => ev.Period.Start).ThenBy(ev => ev.Period.End)) foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate))
{ {
if (ev.Period.Start >= lastEventEnding) if (ev.Period.Start >= lastEventEnding)
{ {

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@@ -21,6 +22,16 @@ namespace Wino.Calendar.Services
private ObservableCollection<GroupedAccountCalendarViewModel> _internalGroupedAccountCalendars = new ObservableCollection<GroupedAccountCalendarViewModel>(); private ObservableCollection<GroupedAccountCalendarViewModel> _internalGroupedAccountCalendars = new ObservableCollection<GroupedAccountCalendarViewModel>();
public IEnumerable<AccountCalendarViewModel> ActiveCalendars
{
get
{
return GroupedAccountCalendars
.SelectMany(a => a.AccountCalendars)
.Where(b => b.IsChecked);
}
}
public AccountCalendarStateService() public AccountCalendarStateService()
{ {
GroupedAccountCalendars = new ReadOnlyObservableCollection<GroupedAccountCalendarViewModel>(_internalGroupedAccountCalendars); GroupedAccountCalendars = new ReadOnlyObservableCollection<GroupedAccountCalendarViewModel>(_internalGroupedAccountCalendars);

View File

@@ -17,7 +17,7 @@
<ResourceDictionary x:Key="Dark"> <ResourceDictionary x:Key="Dark">
<!-- CalendarControl --> <!-- CalendarControl -->
<SolidColorBrush x:Key="CalendarSeperatorBrush">#000000</SolidColorBrush> <SolidColorBrush x:Key="CalendarSeperatorBrush">#525252</SolidColorBrush>
<SolidColorBrush x:Key="CalendarFieldWorkingHoursBackgroundBrush">#32262626</SolidColorBrush> <SolidColorBrush x:Key="CalendarFieldWorkingHoursBackgroundBrush">#32262626</SolidColorBrush>
<SolidColorBrush x:Key="CalendarFieldSelectedBackgroundBrush">#121212</SolidColorBrush> <SolidColorBrush x:Key="CalendarFieldSelectedBackgroundBrush">#121212</SolidColorBrush>

View File

@@ -13,7 +13,11 @@
<!-- Default Calendar Item View Model Template --> <!-- Default Calendar Item View Model Template -->
<DataTemplate x:Key="CalendarItemViewModelItemTemplate" x:DataType="data:CalendarItemViewModel"> <DataTemplate x:Key="CalendarItemViewModelItemTemplate" x:DataType="data:CalendarItemViewModel">
<Grid Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(AssignedCalendar.BackgroundColorHex), Mode=OneWay}" CornerRadius="4"> <Grid
Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
BorderBrush="{ThemeResource CalendarSeperatorBrush}"
BorderThickness="1"
CornerRadius="6">
<TextBlock <TextBlock
Margin="2,0" Margin="2,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
@@ -44,7 +48,7 @@
<ItemsControl ItemTemplate="{StaticResource CalendarItemViewModelItemTemplate}" ItemsSource="{x:Bind EventsCollection.RegularEvents}"> <ItemsControl ItemTemplate="{StaticResource CalendarItemViewModelItemTemplate}" ItemsSource="{x:Bind EventsCollection.RegularEvents}">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<controls:WinoCalendarPanel HourHeight="{Binding Path=CalendarRenderOptions.CalendarSettings.HourHeight}" /> <controls:WinoCalendarPanel HourHeight="{Binding Path=CalendarRenderOptions.CalendarSettings.HourHeight}" Period="{Binding Path=Period}" />
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ItemsControl.ItemsPanel> </ItemsControl.ItemsPanel>
</ItemsControl> </ItemsControl>

View File

@@ -44,11 +44,6 @@ namespace Wino.Calendar.Views
} }
//public void Receive(GoToCalendarDayMessage message)
//{
// CalendarView.GoToDay(message.DateTime);
//}
private void PreviousDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoPreviousDateRequestedMessage()); private void PreviousDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoPreviousDateRequestedMessage());
private void NextDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoNextDateRequestedMessage()); private void NextDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoNextDateRequestedMessage());

View File

@@ -1,5 +1,6 @@
using System; using System;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Itenso.TimePeriod;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation; using Windows.UI.Xaml.Navigation;
@@ -16,7 +17,7 @@ namespace Wino.Calendar.Views
IRecipient<GoNextDateRequestedMessage>, IRecipient<GoNextDateRequestedMessage>,
IRecipient<GoPreviousDateRequestedMessage> IRecipient<GoPreviousDateRequestedMessage>
{ {
private DateTime? selectedDateTime; private DateTimeOffset? selectedDateTime;
public CalendarPage() public CalendarPage()
{ {
InitializeComponent(); InitializeComponent();
@@ -50,7 +51,13 @@ namespace Wino.Calendar.Views
private void CellSelected(object sender, TimelineCellSelectedArgs e) private void CellSelected(object sender, TimelineCellSelectedArgs e)
{ {
// Selected date is in Local kind.
selectedDateTime = e.ClickedDate; selectedDateTime = e.ClickedDate;
var utc = DateTime.SpecifyKind(e.ClickedDate, DateTimeKind.Utc);
var unspecified = DateTime.SpecifyKind(e.ClickedDate, DateTimeKind.Unspecified);
var putc = new TimeRange(utc, utc.AddMinutes(30));
var punspecified = new TimeRange(unspecified, unspecified.AddMinutes(30));
// TODO: Popup is not positioned well on daily view. // TODO: Popup is not positioned well on daily view.
TeachingTipPositionerGrid.Width = e.CellSize.Width; TeachingTipPositionerGrid.Width = e.CellSize.Width;
@@ -59,8 +66,19 @@ namespace Wino.Calendar.Views
Canvas.SetLeft(TeachingTipPositionerGrid, e.PositionerPoint.X); Canvas.SetLeft(TeachingTipPositionerGrid, e.PositionerPoint.X);
Canvas.SetTop(TeachingTipPositionerGrid, e.PositionerPoint.Y); Canvas.SetTop(TeachingTipPositionerGrid, e.PositionerPoint.Y);
// TODO: End time can be from settings. //var testCalendarItem = new CalendarItem
// WeakReferenceMessenger.Default.Send(new CalendarEventAdded(new CalendarItem(selectedDateTime.Value, selectedDateTime.Value.AddMinutes(30)))); //{
// CalendarId = Guid.Parse("9ead7613-dacb-4163-8d33-2e32e65008a1"),
// StartTime = selectedDateTime.Value, // All events are saved in UTC.
// DurationInMinutes = 30,
// CreatedAt = DateTime.UtcNow,
// Description = "Test Description",
// Location = "Poland",
// Title = "Test event",
// Id = Guid.NewGuid()
//};
//WeakReferenceMessenger.Default.Send(new CalendarEventAdded(testCalendarItem));
NewEventTip.IsOpen = true; NewEventTip.IsOpen = true;
} }

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Itenso.TimePeriod;
using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
@@ -12,9 +13,6 @@ namespace Wino.Core.Domain.Collections
public event EventHandler<ICalendarItem> CalendarItemAdded; public event EventHandler<ICalendarItem> CalendarItemAdded;
public event EventHandler<ICalendarItem> CalendarItemRemoved; public event EventHandler<ICalendarItem> CalendarItemRemoved;
public event EventHandler<List<ICalendarItem>> CalendarItemRangeAdded;
public event EventHandler<List<ICalendarItem>> CalendarItemRangeRemoved;
public event EventHandler CalendarItemsCleared; public event EventHandler CalendarItemsCleared;
private ObservableRangeCollection<ICalendarItem> _internalRegularEvents = []; private ObservableRangeCollection<ICalendarItem> _internalRegularEvents = [];
@@ -22,79 +20,97 @@ namespace Wino.Core.Domain.Collections
public ReadOnlyObservableCollection<ICalendarItem> RegularEvents { get; } public ReadOnlyObservableCollection<ICalendarItem> RegularEvents { get; }
public ReadOnlyObservableCollection<ICalendarItem> AllDayEvents { get; } public ReadOnlyObservableCollection<ICalendarItem> AllDayEvents { get; }
public ITimePeriod Period { get; }
public CalendarEventCollection() private readonly List<ICalendarItem> _allItems = new List<ICalendarItem>();
public CalendarEventCollection(ITimePeriod period)
{ {
RegularEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalRegularEvents); RegularEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalRegularEvents);
AllDayEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalAllDayEvents); AllDayEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalAllDayEvents);
Period = period;
} }
public bool HasCalendarEvent(AccountCalendar accountCalendar) public bool HasCalendarEvent(AccountCalendar accountCalendar)
=> _allItems.Any(x => x.AssignedCalendar.Id == accountCalendar.Id);
public void FilterByCalendars(IEnumerable<Guid> visibleCalendarIds)
{ {
return _internalAllDayEvents.Any(x => x.AssignedCalendar.Id == accountCalendar.Id) || foreach (var item in _allItems)
_internalRegularEvents.Any(x => x.AssignedCalendar.Id == accountCalendar.Id); {
var collection = GetProperCollectionForCalendarItem(item);
if (!visibleCalendarIds.Contains(item.AssignedCalendar.Id) && collection.Contains(item))
{
RemoveCalendarItemInternal(collection, item, false);
}
else if (visibleCalendarIds.Contains(item.AssignedCalendar.Id) && !collection.Contains(item))
{
AddCalendarItemInternal(collection, item, false);
}
}
} }
public void AddCalendarItemRange(IEnumerable<ICalendarItem> calendarItems) private ObservableRangeCollection<ICalendarItem> GetProperCollectionForCalendarItem(ICalendarItem calendarItem)
{ {
foreach (var calendarItem in calendarItems) // Event duration is not simply enough to determine whether it's an all-day event or not.
{ // Event may start at 11:00 PM and end next day at 11:00 PM. It's not an all-day event.
AddCalendarItem(calendarItem); // It's a multi-day event.
}
CalendarItemRangeAdded?.Invoke(this, new List<ICalendarItem>(calendarItems)); bool isAllDayEvent = calendarItem.Period.Duration.TotalDays == 1 && calendarItem.Period.Start.TimeOfDay == TimeSpan.Zero;
}
public void RemoveCalendarItemRange(IEnumerable<ICalendarItem> calendarItems) return isAllDayEvent ? _internalAllDayEvents : _internalRegularEvents;
{
foreach (var calendarItem in calendarItems)
{
RemoveCalendarItem(calendarItem);
}
CalendarItemRangeRemoved?.Invoke(this, new List<ICalendarItem>(calendarItems));
} }
public void AddCalendarItem(ICalendarItem calendarItem) public void AddCalendarItem(ICalendarItem calendarItem)
{
var collection = GetProperCollectionForCalendarItem(calendarItem);
AddCalendarItemInternal(collection, calendarItem);
}
public void RemoveCalendarItem(ICalendarItem calendarItem)
{
var collection = GetProperCollectionForCalendarItem(calendarItem);
RemoveCalendarItemInternal(collection, calendarItem);
}
private void AddCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool create = true)
{ {
if (calendarItem is not ICalendarItemViewModel) if (calendarItem is not ICalendarItemViewModel)
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem)); throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
if (calendarItem.Period.Duration.TotalMinutes == 1440) collection.Add(calendarItem);
if (create)
{ {
_internalAllDayEvents.Add(calendarItem); _allItems.Add(calendarItem);
}
else
{
_internalRegularEvents.Add(calendarItem);
} }
CalendarItemAdded?.Invoke(this, calendarItem); CalendarItemAdded?.Invoke(this, calendarItem);
} }
private void RemoveCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool destroy = true)
{
if (calendarItem is not ICalendarItemViewModel)
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
collection.Remove(calendarItem);
if (destroy)
{
_allItems.Remove(calendarItem);
}
CalendarItemRemoved?.Invoke(this, calendarItem);
}
public void Clear() public void Clear()
{ {
_internalAllDayEvents.Clear(); _internalAllDayEvents.Clear();
_internalRegularEvents.Clear(); _internalRegularEvents.Clear();
_allItems.Clear();
CalendarItemsCleared?.Invoke(this, EventArgs.Empty); CalendarItemsCleared?.Invoke(this, EventArgs.Empty);
} }
public void RemoveCalendarItem(ICalendarItem calendarItem)
{
if (calendarItem is not ICalendarItemViewModel)
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
if (calendarItem.Period.Duration.TotalMinutes == 1440)
{
_internalAllDayEvents.Remove(calendarItem);
}
else
{
_internalRegularEvents.Remove(calendarItem);
}
CalendarItemRemoved?.Invoke(this, calendarItem);
}
} }
} }

View File

@@ -13,18 +13,43 @@ namespace Wino.Core.Domain.Entities.Calendar
public string Title { get; set; } public string Title { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string Location { get; set; } public string Location { get; set; }
public DateTimeOffset StartTime { get; set; }
public int DurationInMinutes { get; set; } public DateTime StartDate { get; set; }
public DateTime EndDate
{
get
{
return StartDate.AddSeconds(DurationInSeconds);
}
}
public TimeSpan StartDateOffset { get; set; }
public TimeSpan EndDateOffset { get; set; }
private ITimePeriod _period;
public ITimePeriod Period
{
get
{
_period ??= new TimeRange(StartDate, EndDate);
return _period;
}
}
public double DurationInSeconds { get; set; }
public string Recurrence { get; set; } public string Recurrence { get; set; }
// TODO
public string CustomEventColorHex { get; set; }
public string HtmlLink { get; set; }
public CalendarItemStatus Status { get; set; } public CalendarItemStatus Status { get; set; }
public CalendarItemVisibility Visibility { get; set; } public CalendarItemVisibility Visibility { get; set; }
public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; }
public Guid CalendarId { get; set; } public Guid CalendarId { get; set; }
[Ignore]
public TimeRange Period => new TimeRange(StartTime.Date, StartTime.Date.AddMinutes(DurationInMinutes));
[Ignore] [Ignore]
public IAccountCalendar AssignedCalendar { get; set; } public IAccountCalendar AssignedCalendar { get; set; }
} }

View File

@@ -7,9 +7,10 @@ namespace Wino.Core.Domain.Interfaces
{ {
string Title { get; } string Title { get; }
Guid Id { get; } Guid Id { get; }
DateTimeOffset StartTime { get; }
int DurationInMinutes { get; }
TimeRange Period { get; }
IAccountCalendar AssignedCalendar { get; } IAccountCalendar AssignedCalendar { get; }
DateTime StartDate { get; set; }
DateTime EndDate { get; }
double DurationInSeconds { get; set; }
ITimePeriod Period { get; }
} }
} }

View File

@@ -2,18 +2,20 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Interfaces namespace Wino.Core.Domain.Interfaces
{ {
public interface ICalendarService public interface ICalendarService
{ {
Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId); Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId);
Task<AccountCalendar> GetAccountCalendarAsync(Guid accountCalendarId);
Task DeleteCalendarItemAsync(Guid calendarItemId); Task DeleteCalendarItemAsync(Guid calendarItemId);
Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar); Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar);
Task InsertAccountCalendarAsync(AccountCalendar accountCalendar); Task InsertAccountCalendarAsync(AccountCalendar accountCalendar);
Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar); Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar);
Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees); Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees);
Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, DateTime rangeStart, DateTime rangeEnd); Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, DayRangeRenderModel dayRangeRenderModel);
} }
} }

View File

@@ -10,14 +10,15 @@ namespace Wino.Core.Domain.Models.Calendar
/// </summary> /// </summary>
public class CalendarDayModel public class CalendarDayModel
{ {
public TimeRange Period { get; } public ITimePeriod Period { get; }
public CalendarEventCollection EventsCollection { get; } = new CalendarEventCollection(); public CalendarEventCollection EventsCollection { get; }
public CalendarDayModel(DateTime representingDate, CalendarRenderOptions calendarRenderOptions) public CalendarDayModel(DateTime representingDate, CalendarRenderOptions calendarRenderOptions)
{ {
RepresentingDate = representingDate; RepresentingDate = representingDate;
Period = new TimeRange(representingDate, representingDate.AddDays(1)); Period = new TimeRange(representingDate, representingDate.AddDays(1));
CalendarRenderOptions = calendarRenderOptions; CalendarRenderOptions = calendarRenderOptions;
EventsCollection = new CalendarEventCollection(Period);
} }
public DateTime RepresentingDate { get; } public DateTime RepresentingDate { get; }

View File

@@ -56,25 +56,16 @@ namespace Wino.Core.Domain.Models.Calendar
private void RegisterCalendarDayEvents(CalendarDayModel calendarDayModel) private void RegisterCalendarDayEvents(CalendarDayModel calendarDayModel)
{ {
calendarDayModel.EventsCollection.CalendarItemAdded += CalendarItemAdded; calendarDayModel.EventsCollection.CalendarItemAdded += CalendarItemAdded;
calendarDayModel.EventsCollection.CalendarItemRangeRemoved += CalendarItemRangeRemoved;
calendarDayModel.EventsCollection.CalendarItemRemoved += CalendarItemRemoved; calendarDayModel.EventsCollection.CalendarItemRemoved += CalendarItemRemoved;
calendarDayModel.EventsCollection.CalendarItemRangeAdded += CalendarItemRangeAdded;
} }
// TODO: These handlers have incorrect senders. They should be the CalendarDayModel. // TODO: These handlers have incorrect senders. They should be the CalendarDayModel.
private void CalendarItemRangeAdded(object sender, List<ICalendarItem> e)
=> CalendarDayEventCollectionUpdated?.Invoke(this, sender as CalendarDayModel);
private void CalendarItemRemoved(object sender, ICalendarItem e) private void CalendarItemRemoved(object sender, ICalendarItem e)
=> CalendarDayEventCollectionUpdated?.Invoke(this, sender as CalendarDayModel); => CalendarDayEventCollectionUpdated?.Invoke(this, sender as CalendarDayModel);
private void CalendarItemAdded(object sender, ICalendarItem e) private void CalendarItemAdded(object sender, ICalendarItem e)
=> CalendarDayEventCollectionUpdated?.Invoke(this, sender as CalendarDayModel); => CalendarDayEventCollectionUpdated?.Invoke(this, sender as CalendarDayModel);
private void CalendarItemRangeRemoved(object sender, List<ICalendarItem> e)
=> CalendarDayEventCollectionUpdated?.Invoke(this, sender as CalendarDayModel);
/// <summary> /// <summary>
/// Unregisters all calendar item change listeners to draw the UI for calendar events. /// Unregisters all calendar item change listeners to draw the UI for calendar events.
/// </summary> /// </summary>
@@ -82,9 +73,7 @@ namespace Wino.Core.Domain.Models.Calendar
{ {
foreach (var day in CalendarDays) foreach (var day in CalendarDays)
{ {
day.EventsCollection.CalendarItemRangeRemoved -= CalendarItemRangeRemoved;
day.EventsCollection.CalendarItemRemoved -= CalendarItemRemoved; day.EventsCollection.CalendarItemRemoved -= CalendarItemRemoved;
day.EventsCollection.CalendarItemRangeAdded -= CalendarItemRangeAdded;
day.EventsCollection.CalendarItemAdded -= CalendarItemAdded; day.EventsCollection.CalendarItemAdded -= CalendarItemAdded;
} }
} }

View File

@@ -1,5 +1,5 @@
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Entities.Calendar;
using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Calendar;
namespace Wino.Core.ViewModels namespace Wino.Core.ViewModels
@@ -8,6 +8,6 @@ namespace Wino.Core.ViewModels
{ {
public void Receive(CalendarEventAdded message) => OnCalendarEventAdded(message.CalendarItem); public void Receive(CalendarEventAdded message) => OnCalendarEventAdded(message.CalendarItem);
protected virtual void OnCalendarEventAdded(ICalendarItem calendarItem) { } protected virtual void OnCalendarEventAdded(CalendarItem calendarItem) { }
} }
} }

View File

@@ -209,88 +209,74 @@ namespace Wino.Core.Extensions
/// </summary> /// </summary>
/// <param name="calendarEvent">The Google Calendar Event object.</param> /// <param name="calendarEvent">The Google Calendar Event object.</param>
/// <returns>The start DateTimeOffset of the event, or null if it cannot be determined.</returns> /// <returns>The start DateTimeOffset of the event, or null if it cannot be determined.</returns>
public static DateTimeOffset? GetEventStartDateTimeOffset(this Event calendarEvent) //public static DateTimeOffset GetEventStartDateTimeOffset(this Event calendarEvent)
{ //{
if (calendarEvent == null) // return GetEventDateTimeOffset(calendarEvent.Start);
{ //}
return null;
}
if (calendarEvent.Start != null) public static DateTimeOffset GetEventDateTimeOffset(EventDateTime calendarEvent)
{
if (calendarEvent != null)
{ {
if (calendarEvent.Start.DateTimeDateTimeOffset != null) if (calendarEvent.DateTimeDateTimeOffset != null)
{ {
return calendarEvent.Start.DateTimeDateTimeOffset; // Use the direct DateTimeOffset property! return calendarEvent.DateTimeDateTimeOffset.Value;
} }
else if (calendarEvent.Start.Date != null) else if (calendarEvent.Date != null)
{ {
if (DateTime.TryParse(calendarEvent.Start.Date, out DateTime startDate)) if (DateTime.TryParse(calendarEvent.Date, out DateTime eventDateTime))
{ {
// Date-only events are treated as UTC midnight // Date-only events are treated as UTC midnight
return new DateTimeOffset(startDate, TimeSpan.Zero); return new DateTimeOffset(eventDateTime, TimeSpan.Zero);
} }
else else
{ {
return null; throw new Exception("Invalid date format in Google Calendar event date.");
} }
} }
} }
return null; // Start time not found throw new Exception("Google Calendar event has no date.");
} }
/// <summary> /// <summary>
/// Calculates the duration of a Google Calendar Event in minutes. /// Calculates the duration of a Google Calendar Event in seconds.
/// Handles date-only and date-time events, but *does not* handle recurring events correctly. /// Handles date-only and date-time events, but *does not* handle recurring events correctly.
/// For recurring events, this method will return the duration of the *first* instance. /// For recurring events, this method will return the duration of the *first* instance.
/// </summary> /// </summary>
/// <param name="calendarEvent">The Google Calendar Event object.</param> /// <param name="calendarEvent">The Google Calendar Event object.</param>
/// <returns>The duration of the event in minutes, or null if it cannot be determined.</returns> /// <returns>The duration of the event in minutes, or null if it cannot be determined.</returns>
public static int? GetEventDurationInMinutes(this Event calendarEvent) //public static int GetEventDurationInSeconds(this Event calendarEvent)
{ //{
if (calendarEvent == null) // var start = calendarEvent.GetEventStartDateTimeOffset();
{
return null;
}
DateTimeOffset? start = calendarEvent.GetEventStartDateTimeOffset(); // DateTimeOffset? end = null;
if (start == null) // if (calendarEvent.End != null)
{ // {
return null; // if (calendarEvent.End.DateTimeDateTimeOffset != null)
} // {
// end = calendarEvent.End.DateTimeDateTimeOffset;
// }
// else if (calendarEvent.End.Date != null)
// {
// if (DateTime.TryParse(calendarEvent.End.Date, out DateTime endDate))
// {
// end = new DateTimeOffset(endDate, TimeSpan.Zero);
// }
// else
// {
// throw new Exception("Invalid date format in Google Calendar event end date.");
// }
// }
// }
DateTimeOffset? end = null; // if (end == null) throw new Exception("Google Calendar event has no end date.");
if (calendarEvent.End != null)
{
if (calendarEvent.End.DateTimeDateTimeOffset != null)
{
end = calendarEvent.End.DateTimeDateTimeOffset;
}
else if (calendarEvent.End.Date != null)
{
if (DateTime.TryParse(calendarEvent.End.Date, out DateTime endDate))
{
end = new DateTimeOffset(endDate, TimeSpan.Zero);
}
else
{
return null;
}
}
}
if (end == null)
{
return null;
}
return (int)(end.Value - start.Value).TotalMinutes;
}
// return (int)(end.Value - start).TotalSeconds;
//}
/// <summary> /// <summary>
/// RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545. /// RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545.
///
/// </summary> /// </summary>
/// <returns>___ separated lines.</returns> /// <returns>___ separated lines.</returns>
public static string GetRecurrenceString(this Event calendarEvent) public static string GetRecurrenceString(this Event calendarEvent)

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Google.Apis.Calendar.v3.Data; using Google.Apis.Calendar.v3.Data;
using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Calendar;
@@ -33,22 +34,32 @@ namespace Wino.Core.Integration.Processors
public async Task<CalendarItem> CreateCalendarItemAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount) public async Task<CalendarItem> CreateCalendarItemAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
{ {
var eventStartDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.Start);
var eventEndDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.End);
var totalDurationInSeconds = (eventEndDateTimeOffset - eventStartDateTimeOffset).TotalSeconds;
var calendarItem = new CalendarItem() var calendarItem = new CalendarItem()
{ {
CalendarId = assignedCalendar.Id, CalendarId = assignedCalendar.Id,
CreatedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow,
Description = calendarEvent.Description, Description = calendarEvent.Description,
StartTime = GoogleIntegratorExtensions.GetEventStartDateTimeOffset(calendarEvent) ?? throw new Exception("Event without a start time."),
DurationInMinutes = GoogleIntegratorExtensions.GetEventDurationInMinutes(calendarEvent) ?? throw new Exception("Event without a duration."),
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
StartDate = eventStartDateTimeOffset.DateTime,
StartDateOffset = eventStartDateTimeOffset.Offset,
EndDateOffset = eventEndDateTimeOffset.Offset,
DurationInSeconds = totalDurationInSeconds,
Location = calendarEvent.Location, Location = calendarEvent.Location,
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent), Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
Status = GetStatus(calendarEvent.Status), Status = GetStatus(calendarEvent.Status),
Title = calendarEvent.Summary, Title = calendarEvent.Summary,
UpdatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow,
Visibility = GetVisibility(calendarEvent.Visibility), Visibility = GetVisibility(calendarEvent.Visibility),
HtmlLink = calendarEvent.HtmlLink
}; };
Debug.WriteLine($"({assignedCalendar.Name}) {calendarItem.Title}, Start: {calendarItem.StartDate.ToString("f")}, End: {calendarItem.EndDate.ToString("f")}");
// TODO: There are some edge cases with cancellation here. // TODO: There are some edge cases with cancellation here.
CalendarItemStatus GetStatus(string status) CalendarItemStatus GetStatus(string status)
{ {

View File

@@ -1,9 +1,9 @@
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Entities.Calendar;
namespace Wino.Messaging.Client.Calendar namespace Wino.Messaging.Client.Calendar
{ {
/// <summary> /// <summary>
/// Raised when event is added to database. /// Raised when event is added to database.
/// </summary> /// </summary>
public record CalendarEventAdded(ICalendarItem CalendarItem); public record CalendarEventAdded(CalendarItem CalendarItem);
} }

View File

@@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes; using Ical.Net.DataTypes;
using SqlKata; using SqlKata;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Calendar;
using Wino.Services.Extensions; using Wino.Services.Extensions;
@@ -71,7 +73,7 @@ namespace Wino.Services
}); });
} }
public async Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, DateTime rangeStart, DateTime rangeEnd) public async Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, DayRangeRenderModel dayRangeRenderModel)
{ {
// TODO: We might need to implement caching here. // TODO: We might need to implement caching here.
// I don't know how much of the events we'll have in total, but this logic scans all events every time. // I don't know how much of the events we'll have in total, but this logic scans all events every time.
@@ -84,19 +86,22 @@ namespace Wino.Services
ev.AssignedCalendar = calendar; ev.AssignedCalendar = calendar;
// Parse recurrence rules // Parse recurrence rules
var calendarEvent = new Ical.Net.CalendarComponents.CalendarEvent var calendarEvent = new CalendarEvent
{ {
Start = new CalDateTime(ev.StartTime.UtcDateTime), Start = new CalDateTime(ev.StartDate),
Duration = TimeSpan.FromMinutes(ev.DurationInMinutes), End = new CalDateTime(ev.EndDate),
}; };
if (string.IsNullOrEmpty(ev.Recurrence)) if (string.IsNullOrEmpty(ev.Recurrence))
{ {
// No recurrence, only check if we fall into the date range. // No recurrence, only check if we fall into the given period.
// All events are saved in UTC, so we need to convert the range to UTC as well.
if (ev.StartTime.UtcDateTime < rangeEnd if (ev.Period.OverlapsWith(dayRangeRenderModel.Period))
&& ev.StartTime.UtcDateTime.AddMinutes(ev.DurationInMinutes) > rangeStart)
{ {
// TODO: We overlap, but this might be a multi-day event.
// Should we split the events here or in panel?
// For now just continue.
result.Add(ev); result.Add(ev);
} }
} }
@@ -110,7 +115,7 @@ namespace Wino.Services
} }
// Calculate occurrences in the range. // Calculate occurrences in the range.
var occurrences = calendarEvent.GetOccurrences(rangeStart, rangeEnd); var occurrences = calendarEvent.GetOccurrences(dayRangeRenderModel.Period.Start, dayRangeRenderModel.Period.End);
foreach (var occurrence in occurrences) foreach (var occurrence in occurrences)
{ {
@@ -121,5 +126,8 @@ namespace Wino.Services
return result; return result;
} }
public Task<AccountCalendar> GetAccountCalendarAsync(Guid accountCalendarId)
=> Connection.GetAsync<AccountCalendar>(accountCalendarId);
} }
} }