Displaying events and all-day events.

This commit is contained in:
Burak Kaan Köse
2024-12-28 23:17:16 +01:00
parent 95b8f54b27
commit 979a3d8f1f
17 changed files with 195 additions and 31 deletions

View File

@@ -34,6 +34,7 @@ namespace Wino.Calendar.ViewModels
// Get rid of some of the items if we have too many. // Get rid of some of the items if we have too many.
private const int maxDayRangeSize = 10; private const int maxDayRangeSize = 10;
private readonly ICalendarService _calendarService;
private readonly IPreferencesService _preferencesService; private readonly IPreferencesService _preferencesService;
// Store latest rendered options. // Store latest rendered options.
@@ -47,9 +48,11 @@ namespace Wino.Calendar.ViewModels
public IStatePersistanceService StatePersistanceService { get; } public IStatePersistanceService StatePersistanceService { get; }
public CalendarPageViewModel(IStatePersistanceService statePersistanceService, public CalendarPageViewModel(IStatePersistanceService statePersistanceService,
ICalendarService calendarService,
IPreferencesService preferencesService) IPreferencesService preferencesService)
{ {
StatePersistanceService = statePersistanceService; StatePersistanceService = statePersistanceService;
_calendarService = calendarService;
_preferencesService = preferencesService; _preferencesService = preferencesService;
_currentSettings = _preferencesService.GetCurrentCalendarSettings(); _currentSettings = _preferencesService.GetCurrentCalendarSettings();
@@ -187,6 +190,8 @@ namespace Wino.Calendar.ViewModels
} }
} }
// Create day ranges for each flip item until we reach the total days to load. // Create day ranges for each flip item until we reach the total days to load.
int totalFlipItemCount = (int)Math.Ceiling((double)flipLoadRange.TotalDays / eachFlipItemCount); int totalFlipItemCount = (int)Math.Ceiling((double)flipLoadRange.TotalDays / eachFlipItemCount);
@@ -203,6 +208,21 @@ namespace Wino.Calendar.ViewModels
renderModels.Add(new DayRangeRenderModel(renderOptions)); renderModels.Add(new DayRangeRenderModel(renderOptions));
} }
// Dates are loaded. Now load the events for them.
foreach (var renderModel in renderModels)
{
foreach (var day in renderModel.CalendarDays)
{
var events = await _calendarService.GetCalendarEventsAsync(Guid.Parse("13e8e385-a1bb-4764-95b4-757901cad35a"), day.Period.Start, day.Period.End).ConfigureAwait(false);
foreach (var calendarItem in events)
{
day.EventsCollection.Add(calendarItem);
}
}
}
CalendarLoadDirection animationDirection = calendarLoadDirection; CalendarLoadDirection animationDirection = calendarLoadDirection;
bool removeCurrent = calendarLoadDirection == CalendarLoadDirection.Replace; bool removeCurrent = calendarLoadDirection == CalendarLoadDirection.Replace;

View File

@@ -0,0 +1,27 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using Itenso.TimePeriod;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels.Data
{
public partial class CalendarItemViewModel : ObservableObject, ICalendarItem
{
public ICalendarItem CalendarItem { get; }
public string Title => CalendarItem.Title;
public Guid Id => CalendarItem.Id;
public DateTimeOffset StartTime => CalendarItem.StartTime;
public int DurationInMinutes => CalendarItem.DurationInMinutes;
public TimeRange Period => CalendarItem.Period;
public CalendarItemViewModel(ICalendarItem calendarItem)
{
CalendarItem = calendarItem;
}
}
}

View File

@@ -20,9 +20,8 @@
<ResourceDictionary Source="Styles/CalendarRenderStyles.xaml" /> <ResourceDictionary Source="Styles/CalendarRenderStyles.xaml" />
<ResourceDictionary Source="Styles/CalendarDayItemsControl.xaml" /> <ResourceDictionary Source="Styles/CalendarDayItemsControl.xaml" />
<ResourceDictionary Source="Styles/DayHeaderControl.xaml" /> <styles:DayColumnControlResources />
<ResourceDictionary Source="Styles/WinoDayTimelineCanvas.xaml" /> <ResourceDictionary Source="Styles/WinoDayTimelineCanvas.xaml" />
<ResourceDictionary Source="Styles/DayColumnControl.xaml" />
<ResourceDictionary Source="Styles/WinoCalendarView.xaml" /> <ResourceDictionary Source="Styles/WinoCalendarView.xaml" />
<ResourceDictionary Source="Styles/WinoCalendarTypeSelectorControl.xaml" /> <ResourceDictionary Source="Styles/WinoCalendarTypeSelectorControl.xaml" />

View File

@@ -29,12 +29,12 @@ namespace Wino.Calendar.Controls
{ {
if (e.OldValue != null && e.OldValue is CalendarDayModel oldCalendarDayModel) if (e.OldValue != null && e.OldValue is CalendarDayModel oldCalendarDayModel)
{ {
control.DetachCollection(oldCalendarDayModel.Events); control.DetachCollection(oldCalendarDayModel.EventsCollection);
} }
if (e.NewValue != null && e.NewValue is CalendarDayModel newCalendarDayModel) if (e.NewValue != null && e.NewValue is CalendarDayModel newCalendarDayModel)
{ {
control.AttachCollection(newCalendarDayModel.Events); control.AttachCollection(newCalendarDayModel.EventsCollection);
} }
control.ResetItems(); control.ResetItems();
@@ -109,13 +109,13 @@ namespace Wino.Calendar.Controls
private void RenderCalendarItems() private void RenderCalendarItems()
{ {
if (DayModel == null || DayModel.Events == null || DayModel.Events.Count == 0) if (DayModel == null || DayModel.EventsCollection == null || DayModel.EventsCollection.Count == 0)
{ {
ResetItems(); ResetItems();
return; return;
} }
foreach (var item in DayModel.Events) foreach (var item in DayModel.EventsCollection)
{ {
AddItem(item); AddItem(item);
} }

View File

@@ -41,12 +41,12 @@ namespace Wino.Calendar.Controls
// We need to listen for new events being added or removed from the collection to reset measurements. // We need to listen for new events being added or removed from the collection to reset measurements.
if (e.OldValue is CalendarDayModel oldDayModel) if (e.OldValue is CalendarDayModel oldDayModel)
{ {
control.DetachCollection(oldDayModel.Events); control.DetachCollection(oldDayModel.EventsCollection);
} }
if (e.NewValue is CalendarDayModel newDayModel) if (e.NewValue is CalendarDayModel newDayModel)
{ {
control.AttachCollection(newDayModel.Events); control.AttachCollection(newDayModel.EventsCollection);
} }
control.ResetMeasurements(); control.ResetMeasurements();
@@ -114,12 +114,17 @@ namespace Wino.Calendar.Controls
var calendarControls = Children.Cast<CalendarItemControl>(); var calendarControls = Children.Cast<CalendarItemControl>();
if (_measurements.Count == 0 && DayModel.Events.Count > 0) // We need to exclude all-day events from the layout algorithm.
// All-day events are displayed in a separate panel.
calendarControls = calendarControls.Where(x => x.Item.DurationInMinutes != 1440);
if (_measurements.Count == 0 && DayModel.EventsCollection.Count > 0)
{ {
// We keep track of this collection when event is added/removed/reset etc. // We keep track of this collection when event is added/removed/reset etc.
// So if the collection is empty, we must fill it up again for proper calculations. // So if the collection is empty, we must fill it up again for proper calculations.
LayoutEvents(DayModel.Events); LayoutEvents(DayModel.EventsCollection);
} }
foreach (var child in calendarControls) foreach (var child in calendarControls)
@@ -129,9 +134,7 @@ namespace Wino.Calendar.Controls
var childMeasurement = _measurements[child.Item.Id]; var childMeasurement = _measurements[child.Item.Id];
// TODO Math.Max(0, GetChildHeight(child.Item.StartTime, child.Item.EndTime)); double childHeight = Math.Max(0, GetChildHeight(child.Item.StartTime, child.Item.StartTime.AddMinutes(child.Item.DurationInMinutes)));
// Recurring events may not have an end time. We need to calculate the height based on the start time and duration.
double childHeight = 50;
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.Item.StartTime, availableHeight)); double childTop = Math.Max(0, GetChildTopMargin(child.Item.StartTime, availableHeight));
double childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth)); double childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth));
@@ -139,12 +142,10 @@ namespace Wino.Calendar.Controls
bool isHorizontallyLastItem = childMeasurement.Right == 1; bool isHorizontallyLastItem = childMeasurement.Right == 1;
// Add additional right margin to items that falls on the right edge of the panel. // Add additional right margin to items that falls on the right edge of the panel.
// Max of 5% of the width or 20px. // Max of 5% of the width or 20px max.
var extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0; var extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0;
var finalChildWidth = childWidth - extraRightMargin; if (childWidth < 0) childWidth = 1;
if (finalChildWidth < 0) finalChildWidth = 1;
child.Measure(new Size(childWidth, childHeight)); child.Measure(new Size(childWidth, childHeight));
@@ -178,6 +179,8 @@ namespace Wino.Calendar.Controls
foreach (var ev in events.OrderBy(ev => ev.Period.Start).ThenBy(ev => ev.Period.End)) foreach (var ev in events.OrderBy(ev => ev.Period.Start).ThenBy(ev => ev.Period.End))
{ {
if (ev.Period.Duration.TotalMinutes == 1440) continue;
if (ev.Period.Start >= lastEventEnding) if (ev.Period.Start >= lastEventEnding)
{ {
PackEvents(columns); PackEvents(columns);

View File

@@ -18,7 +18,8 @@
<TextBlock <TextBlock
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Bind Item.Title, Mode=OneWay}" /> Text="{x:Bind Item.Title, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
</Grid> </Grid>
</ControlTemplate> </ControlTemplate>
</Setter.Value> </Setter.Value>

View File

@@ -1,14 +1,17 @@
<ResourceDictionary <ResourceDictionary
x:Class="Wino.Calendar.Styles.DayColumnControlResources"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Calendar.Controls"> xmlns:controls="using:Wino.Calendar.Controls"
xmlns:data="using:Wino.Calendar.ViewModels.Data"
xmlns:interfaces="using:Wino.Core.Domain.Interfaces"
xmlns:local="using:Wino.Calendar.Styles">
<!-- Top column header DayColumnControl --> <!-- Top column header DayColumnControl -->
<Style TargetType="controls:DayColumnControl"> <Style TargetType="controls:DayColumnControl">
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="controls:DayColumnControl"> <ControlTemplate TargetType="controls:DayColumnControl">
<Grid MinHeight="100" MaxHeight="150"> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="25" /> <RowDefinition Height="25" />
<RowDefinition Height="7" /> <RowDefinition Height="7" />
@@ -62,11 +65,20 @@
<!-- Extras --> <!-- Extras -->
<StackPanel Grid.Column="1" HorizontalAlignment="Right" /> <StackPanel Grid.Column="1" HorizontalAlignment="Right" />
<!-- Events --> <!-- All-Day Events -->
<ScrollViewer <ScrollViewer
Grid.Row="1" Grid.Row="1"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
VerticalScrollBarVisibility="Hidden" /> VerticalScrollBarVisibility="Hidden">
<ItemsControl ItemsSource="{Binding EventsCollection.AllDayEvents, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<!-- All-Day Event template -->
<DataTemplate x:DataType="interfaces:ICalendarItem">
<TextBlock Text="{x:Bind Title}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid> </Grid>
<VisualStateManager.VisualStateGroups> <VisualStateManager.VisualStateGroups>
@@ -89,5 +101,4 @@
</Style> </Style>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -0,0 +1,12 @@
using Windows.UI.Xaml;
namespace Wino.Calendar.Styles
{
partial class DayColumnControlResources : ResourceDictionary
{
public DayColumnControlResources()
{
InitializeComponent();
}
}
}

View File

@@ -42,7 +42,10 @@
ItemsSource="{x:Bind CalendarDays}"> ItemsSource="{x:Bind CalendarDays}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:CalendarDayModel"> <DataTemplate x:DataType="models:CalendarDayModel">
<controls:DayColumnControl DayModel="{x:Bind}" /> <controls:DayColumnControl
MinHeight="100"
MaxHeight="200"
DayModel="{x:Bind}" />
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>

View File

@@ -158,6 +158,9 @@
<Compile Include="Services\ProviderService.cs" /> <Compile Include="Services\ProviderService.cs" />
<Compile Include="Services\SettingsBuilderService.cs" /> <Compile Include="Services\SettingsBuilderService.cs" />
<Compile Include="Styles\CalendarItemControlResources.xaml.cs" /> <Compile Include="Styles\CalendarItemControlResources.xaml.cs" />
<Compile Include="Styles\DayColumnControlResources.xaml.cs">
<DependentUpon>DayColumnControlResources.xaml</DependentUpon>
</Compile>
<Compile Include="Styles\WinoCalendarResources.xaml.cs" /> <Compile Include="Styles\WinoCalendarResources.xaml.cs" />
<Compile Include="Views\Abstract\AccountManagementPageAbstract.cs" /> <Compile Include="Views\Abstract\AccountManagementPageAbstract.cs" />
<Compile Include="Views\Abstract\AppShellAbstract.cs" /> <Compile Include="Views\Abstract\AppShellAbstract.cs" />
@@ -261,9 +264,9 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
</Page> </Page>
<Page Include="Styles\DayColumnControl.xaml"> <Page Include="Styles\DayColumnControlResources.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Styles\DayHeaderControl.xaml"> <Page Include="Styles\DayHeaderControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>

View File

@@ -0,0 +1,29 @@
using System.Collections.ObjectModel;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Collections
{
public class CalendarEventCollection : ObservableRangeCollection<ICalendarItem>
{
public ObservableCollection<ICalendarItem> AllDayEvents { get; } = new ObservableCollection<ICalendarItem>();
public new void Add(ICalendarItem calendarItem)
{
base.Add(calendarItem);
if (calendarItem.Period.Duration.TotalMinutes == 1440)
{
AllDayEvents.Add(calendarItem);
}
}
public new void Remove(ICalendarItem calendarItem)
{
base.Remove(calendarItem);
if (calendarItem.Period.Duration.TotalMinutes == 1440)
{
AllDayEvents.Remove(calendarItem);
}
}
}
}

View File

@@ -8,6 +8,8 @@
public const string WinoLocalDraftHeader = "X-Wino-Draft-Id"; public const string WinoLocalDraftHeader = "X-Wino-Draft-Id";
public const string LocalDraftStartPrefix = "localDraft_"; public const string LocalDraftStartPrefix = "localDraft_";
public const string CalendarEventRecurrenceRuleSeperator = "___";
public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey); public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey);
public const string ToastActionKey = nameof(ToastActionKey); public const string ToastActionKey = nameof(ToastActionKey);

View File

@@ -14,5 +14,6 @@ namespace Wino.Core.Domain.Interfaces
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<ICalendarItem>> GetCalendarEventsAsync(Guid calendarId, DateTime rangeStart, DateTime rangeEnd);
} }
} }

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.ObjectModel;
using Itenso.TimePeriod; using Itenso.TimePeriod;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Collections;
namespace Wino.Core.Domain.Models.Calendar namespace Wino.Core.Domain.Models.Calendar
{ {
@@ -12,7 +11,7 @@ namespace Wino.Core.Domain.Models.Calendar
public class CalendarDayModel public class CalendarDayModel
{ {
public TimeRange Period { get; } public TimeRange Period { get; }
public ObservableCollection<ICalendarItem> Events { get; } = new ObservableCollection<ICalendarItem>(); public CalendarEventCollection EventsCollection { get; } = new CalendarEventCollection();
public CalendarDayModel(DateTime representingDate, CalendarRenderOptions calendarRenderOptions) public CalendarDayModel(DateTime representingDate, CalendarRenderOptions calendarRenderOptions)
{ {
RepresentingDate = representingDate; RepresentingDate = representingDate;

View File

@@ -5,6 +5,7 @@ using System.Web;
using Google.Apis.Calendar.v3.Data; using Google.Apis.Calendar.v3.Data;
using Google.Apis.Gmail.v1.Data; using Google.Apis.Gmail.v1.Data;
using MimeKit; using MimeKit;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
@@ -278,7 +279,7 @@ namespace Wino.Core.Extensions
return null; return null;
} }
return string.Join("___", calendarEvent.Recurrence); return string.Join(Constants.CalendarEventRecurrenceRuleSeperator, calendarEvent.Recurrence);
} }
} }
} }

View File

@@ -1,8 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Ical.Net.DataTypes;
using SqlKata; using SqlKata;
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.Messaging.Client.Calendar; using Wino.Messaging.Client.Calendar;
@@ -67,5 +70,54 @@ namespace Wino.Services
conn.InsertAll(attendees); conn.InsertAll(attendees);
}); });
} }
public async Task<List<ICalendarItem>> GetCalendarEventsAsync(Guid calendarId, DateTime rangeStart, DateTime rangeEnd)
{
// 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.
var accountEvents = await Connection.Table<CalendarItem>().Where(x => x.CalendarId == calendarId).ToListAsync();
var result = new List<ICalendarItem>();
foreach (var ev in accountEvents)
{
// Parse recurrence rules
var calendarEvent = new Ical.Net.CalendarComponents.CalendarEvent
{
Start = new CalDateTime(ev.StartTime.UtcDateTime),
Duration = TimeSpan.FromMinutes(ev.DurationInMinutes),
};
if (string.IsNullOrEmpty(ev.Recurrence))
{
// No recurrence, only check if we fall into the date range.
// All events are saved in UTC, so we need to convert the range to UTC as well.
if (ev.StartTime.UtcDateTime < rangeEnd
&& ev.StartTime.UtcDateTime.AddMinutes(ev.DurationInMinutes) > rangeStart)
{
result.Add(ev);
}
}
else
{
var recurrenceLines = Regex.Split(ev.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
foreach (var line in recurrenceLines)
{
calendarEvent.RecurrenceRules.Add(new RecurrencePattern(line));
}
// Calculate occurrences in the range.
var occurrences = calendarEvent.GetOccurrences(rangeStart, rangeEnd);
foreach (var occurrence in occurrences)
{
result.Add(ev);
}
}
}
return result;
}
} }
} }

View File

@@ -7,6 +7,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.70" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.70" />
<PackageReference Include="Ical.Net" Version="4.3.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Serilog" Version="4.1.0" /> <PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" /> <PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />