Handling of multi-day events, new rendering etc.
This commit is contained in:
@@ -27,6 +27,7 @@ namespace Wino.Calendar.ViewModels
|
|||||||
{
|
{
|
||||||
public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||||
IRecipient<LoadCalendarMessage>,
|
IRecipient<LoadCalendarMessage>,
|
||||||
|
IRecipient<CalendarItemDeleted>,
|
||||||
IRecipient<CalendarSettingsUpdatedMessage>,
|
IRecipient<CalendarSettingsUpdatedMessage>,
|
||||||
IRecipient<CalendarItemTappedMessage>,
|
IRecipient<CalendarItemTappedMessage>,
|
||||||
IRecipient<CalendarItemDoubleTappedMessage>,
|
IRecipient<CalendarItemDoubleTappedMessage>,
|
||||||
@@ -90,7 +91,7 @@ namespace Wino.Calendar.ViewModels
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private ObservableRangeCollection<DayRangeRenderModel> _dayRanges = [];
|
private DayRangeCollection _dayRanges = [];
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private int _selectedDateRangeIndex;
|
private int _selectedDateRangeIndex;
|
||||||
@@ -149,8 +150,6 @@ namespace Wino.Calendar.ViewModels
|
|||||||
days.ForEach(a => a.EventsCollection.FilterByCalendars(AccountCalendarStateService.ActiveCalendars.Select(a => a.Id)));
|
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.
|
||||||
// Should be a field ideally.
|
// Should be a field ideally.
|
||||||
private BaseCalendarTypeDrawingStrategy GetDrawingStrategy(CalendarDisplayType displayType)
|
private BaseCalendarTypeDrawingStrategy GetDrawingStrategy(CalendarDisplayType displayType)
|
||||||
@@ -256,14 +255,13 @@ namespace Wino.Calendar.ViewModels
|
|||||||
// 2. Day display count is different.
|
// 2. Day display count is different.
|
||||||
// 3. Display date is not in the visible range.
|
// 3. Display date is not in the visible range.
|
||||||
|
|
||||||
var loadedRange = GetLoadedDateRange();
|
|
||||||
|
|
||||||
if (loadedRange == null) return false;
|
if (DayRanges.DisplayRange == null) return false;
|
||||||
|
|
||||||
return
|
return
|
||||||
(_currentDisplayType != StatePersistanceService.CalendarDisplayType ||
|
(_currentDisplayType != StatePersistanceService.CalendarDisplayType ||
|
||||||
_displayDayCount != StatePersistanceService.DayDisplayCount ||
|
_displayDayCount != StatePersistanceService.DayDisplayCount ||
|
||||||
!(message.DisplayDate >= loadedRange.StartDate && message.DisplayDate <= loadedRange.EndDate));
|
!(message.DisplayDate >= DayRanges.DisplayRange.StartDate && message.DisplayDate <= DayRanges.DisplayRange.EndDate));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void Receive(LoadCalendarMessage message)
|
public async void Receive(LoadCalendarMessage message)
|
||||||
@@ -305,20 +303,10 @@ namespace Wino.Calendar.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DateRange GetLoadedDateRange()
|
|
||||||
{
|
|
||||||
if (DayRanges.Count == 0) return null;
|
|
||||||
|
|
||||||
var minimumLoadedDate = DayRanges[0].CalendarRenderOptions.DateRange.StartDate;
|
|
||||||
var maximumLoadedDate = DayRanges[DayRanges.Count - 1].CalendarRenderOptions.DateRange.EndDate;
|
|
||||||
|
|
||||||
return new DateRange(minimumLoadedDate, maximumLoadedDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel)
|
private async Task AddDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel)
|
||||||
{
|
{
|
||||||
dayRangeRenderModel.CalendarDayEventCollectionUpdated -= EventsUpdatedInDayHeader;
|
if (dayRangeRenderModel == null) return;
|
||||||
dayRangeRenderModel.CalendarDayEventCollectionUpdated += EventsUpdatedInDayHeader;
|
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(() =>
|
||||||
{
|
{
|
||||||
@@ -330,9 +318,6 @@ namespace Wino.Calendar.ViewModels
|
|||||||
{
|
{
|
||||||
if (dayRangeRenderModel == null) return;
|
if (dayRangeRenderModel == null) return;
|
||||||
|
|
||||||
dayRangeRenderModel.CalendarDayEventCollectionUpdated -= EventsUpdatedInDayHeader;
|
|
||||||
dayRangeRenderModel.CalendarDayEventCollectionUpdated += EventsUpdatedInDayHeader;
|
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(() =>
|
||||||
{
|
{
|
||||||
DayRanges.Insert(index, dayRangeRenderModel);
|
DayRanges.Insert(index, dayRangeRenderModel);
|
||||||
@@ -343,9 +328,6 @@ namespace Wino.Calendar.ViewModels
|
|||||||
{
|
{
|
||||||
if (dayRangeRenderModel == null) return;
|
if (dayRangeRenderModel == null) return;
|
||||||
|
|
||||||
dayRangeRenderModel.CalendarDayEventCollectionUpdated -= EventsUpdatedInDayHeader;
|
|
||||||
dayRangeRenderModel.UnregisterAll();
|
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(() =>
|
||||||
{
|
{
|
||||||
DayRanges.Remove(dayRangeRenderModel);
|
DayRanges.Remove(dayRangeRenderModel);
|
||||||
@@ -354,14 +336,6 @@ namespace Wino.Calendar.ViewModels
|
|||||||
|
|
||||||
private async Task ClearDayRangeModelsAsync()
|
private async Task ClearDayRangeModelsAsync()
|
||||||
{
|
{
|
||||||
// Unregister all events and clear the list directly.
|
|
||||||
|
|
||||||
foreach (var dayRangeModel in DayRanges)
|
|
||||||
{
|
|
||||||
dayRangeModel.CalendarDayEventCollectionUpdated -= EventsUpdatedInDayHeader;
|
|
||||||
dayRangeModel.UnregisterAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(() =>
|
||||||
{
|
{
|
||||||
DayRanges.Clear();
|
DayRanges.Clear();
|
||||||
@@ -397,9 +371,8 @@ namespace Wino.Calendar.ViewModels
|
|||||||
|
|
||||||
DateRange flipLoadRange = null;
|
DateRange flipLoadRange = null;
|
||||||
|
|
||||||
var initializedDateRange = GetLoadedDateRange();
|
|
||||||
|
|
||||||
if (calendarInitInitiative == CalendarInitInitiative.User || initializedDateRange == null)
|
if (calendarInitInitiative == CalendarInitInitiative.User || DayRanges.DisplayRange == null)
|
||||||
{
|
{
|
||||||
flipLoadRange = strategy.GetRenderDateRange(displayDate, StatePersistanceService.DayDisplayCount);
|
flipLoadRange = strategy.GetRenderDateRange(displayDate, StatePersistanceService.DayDisplayCount);
|
||||||
}
|
}
|
||||||
@@ -411,11 +384,11 @@ namespace Wino.Calendar.ViewModels
|
|||||||
|
|
||||||
if (calendarLoadDirection == CalendarLoadDirection.Previous)
|
if (calendarLoadDirection == CalendarLoadDirection.Previous)
|
||||||
{
|
{
|
||||||
flipLoadRange = strategy.GetPreviousDateRange(initializedDateRange, StatePersistanceService.DayDisplayCount);
|
flipLoadRange = strategy.GetPreviousDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
flipLoadRange = strategy.GetNextDateRange(initializedDateRange, StatePersistanceService.DayDisplayCount);
|
flipLoadRange = strategy.GetNextDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,21 +492,14 @@ namespace Wino.Calendar.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO...
|
|
||||||
private void EventsUpdatedInDayHeader(object sender, CalendarDayModel e)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async void OnCalendarItemAdded(CalendarItem calendarItem)
|
protected override async void OnCalendarItemAdded(CalendarItem calendarItem)
|
||||||
{
|
{
|
||||||
base.OnCalendarItemAdded(calendarItem);
|
base.OnCalendarItemAdded(calendarItem);
|
||||||
|
|
||||||
// Check if event falls into the current date range.
|
// Check if event falls into the current date range.
|
||||||
|
|
||||||
var loadedDateRange = GetLoadedDateRange();
|
|
||||||
|
|
||||||
if (loadedDateRange == null) return;
|
if (DayRanges.DisplayRange == null) return;
|
||||||
|
|
||||||
// Check whether this event falls into any of the loaded date ranges.
|
// Check whether this event falls into any of the loaded date ranges.
|
||||||
var allDaysForEvent = DayRanges.SelectMany(a => a.CalendarDays).Where(a => a.Period.OverlapsWith(calendarItem.Period));
|
var allDaysForEvent = DayRanges.SelectMany(a => a.CalendarDays).Where(a => a.Period.OverlapsWith(calendarItem.Period));
|
||||||
@@ -576,8 +542,6 @@ namespace Wino.Calendar.ViewModels
|
|||||||
foreach (var @event in events)
|
foreach (var @event in events)
|
||||||
{
|
{
|
||||||
// Find the days that the event falls into.
|
// Find the days that the event falls into.
|
||||||
// TODO: Multi-day events are not fully supported yet.
|
|
||||||
|
|
||||||
var allDaysForEvent = dayRangeRenderModel.CalendarDays.Where(a => a.Period.OverlapsWith(@event.Period));
|
var allDaysForEvent = dayRangeRenderModel.CalendarDays.Where(a => a.Period.OverlapsWith(@event.Period));
|
||||||
|
|
||||||
foreach (var calendarDay in allDaysForEvent)
|
foreach (var calendarDay in allDaysForEvent)
|
||||||
@@ -628,13 +592,11 @@ namespace Wino.Calendar.ViewModels
|
|||||||
// Nothing to scroll.
|
// Nothing to scroll.
|
||||||
if (DayRanges.Count == 0) return false;
|
if (DayRanges.Count == 0) return false;
|
||||||
|
|
||||||
var initializedDateRange = GetLoadedDateRange();
|
if (DayRanges.DisplayRange == null) return false;
|
||||||
|
|
||||||
if (initializedDateRange == null) return false;
|
|
||||||
|
|
||||||
var selectedDate = message.DisplayDate;
|
var selectedDate = message.DisplayDate;
|
||||||
|
|
||||||
return selectedDate >= initializedDateRange.StartDate && selectedDate <= initializedDateRange.EndDate;
|
return selectedDate >= DayRanges.DisplayRange.StartDate && selectedDate <= DayRanges.DisplayRange.EndDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnIsAllDayChanged(bool value)
|
partial void OnIsAllDayChanged(bool value)
|
||||||
@@ -803,5 +765,22 @@ namespace Wino.Calendar.ViewModels
|
|||||||
public void Receive(CalendarItemRightTappedMessage message)
|
public void Receive(CalendarItemRightTappedMessage message)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async void Receive(CalendarItemDeleted message)
|
||||||
|
{
|
||||||
|
// Each deleted recurrence will report for it's own.
|
||||||
|
|
||||||
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
var deletedItem = message.CalendarItem;
|
||||||
|
|
||||||
|
// Event might be spreaded into multiple days.
|
||||||
|
// Remove from all.
|
||||||
|
|
||||||
|
var calendarItems = GetCalendarItems(deletedItem.Id);
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ namespace Wino.Calendar.ViewModels
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _is24HourHeaders;
|
private bool _is24HourHeaders;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _ghostRenderAllDayEvents;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private TimeSpan _workingHourStart;
|
private TimeSpan _workingHourStart;
|
||||||
|
|
||||||
@@ -61,6 +64,7 @@ namespace Wino.Calendar.ViewModels
|
|||||||
_workingHourStart = preferencesService.WorkingHourStart;
|
_workingHourStart = preferencesService.WorkingHourStart;
|
||||||
_workingHourEnd = preferencesService.WorkingHourEnd;
|
_workingHourEnd = preferencesService.WorkingHourEnd;
|
||||||
_cellHourHeight = preferencesService.HourHeight;
|
_cellHourHeight = preferencesService.HourHeight;
|
||||||
|
_ghostRenderAllDayEvents = preferencesService.GhostRenderAllDayEvents;
|
||||||
|
|
||||||
_workingDayStartIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
|
_workingDayStartIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
|
||||||
_workingDayEndIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
|
_workingDayEndIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
|
||||||
@@ -75,6 +79,7 @@ namespace Wino.Calendar.ViewModels
|
|||||||
partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings();
|
partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings();
|
||||||
partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings();
|
partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings();
|
||||||
partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
|
partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
|
||||||
|
partial void OnGhostRenderAllDayEventsChanged(bool value) => SaveSettings();
|
||||||
|
|
||||||
public void SaveSettings()
|
public void SaveSettings()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,5 +37,7 @@ namespace Wino.Calendar.ViewModels.Data
|
|||||||
{
|
{
|
||||||
CalendarItem = calendarItem;
|
CalendarItem = calendarItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString() => CalendarItem.Title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,47 @@
|
|||||||
xmlns:domain="using:Wino.Core.Domain"
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
xmlns:helpers="using:Wino.Helpers"
|
xmlns:helpers="using:Wino.Helpers"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:selectors="using:Wino.Calendar.Selectors"
|
||||||
|
x:Name="AllDayControl"
|
||||||
d:DesignHeight="300"
|
d:DesignHeight="300"
|
||||||
d:DesignWidth="400"
|
d:DesignWidth="400"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<controls:CalendarItemControl
|
<ItemsControl x:Name="EventItemsControl" ItemsSource="{x:Bind CalendarDayModel.EventsCollection.AllDayEvents, Mode=OneWay}">
|
||||||
|
<ItemsControl.ItemTemplateSelector>
|
||||||
|
<selectors:CustomAreaCalendarItemSelector>
|
||||||
|
<selectors:CustomAreaCalendarItemSelector.AllDayTemplate>
|
||||||
|
<DataTemplate x:DataType="data:CalendarItemViewModel">
|
||||||
|
<controls:CalendarItemControl
|
||||||
|
CalendarItem="{x:Bind}"
|
||||||
|
DisplayingDate="{Binding CalendarDayModel, ElementName=AllDayControl}"
|
||||||
|
IsCustomEventArea="True" />
|
||||||
|
</DataTemplate>
|
||||||
|
</selectors:CustomAreaCalendarItemSelector.AllDayTemplate>
|
||||||
|
<selectors:CustomAreaCalendarItemSelector.MultiDayTemplate>
|
||||||
|
<DataTemplate x:DataType="data:CalendarItemViewModel">
|
||||||
|
<controls:CalendarItemControl
|
||||||
|
CalendarItem="{x:Bind}"
|
||||||
|
DisplayingDate="{Binding CalendarDayModel, ElementName=AllDayControl}"
|
||||||
|
IsCustomEventArea="True" />
|
||||||
|
</DataTemplate>
|
||||||
|
</selectors:CustomAreaCalendarItemSelector.MultiDayTemplate>
|
||||||
|
</selectors:CustomAreaCalendarItemSelector>
|
||||||
|
</ItemsControl.ItemTemplateSelector>
|
||||||
|
<ItemsControl.ItemContainerTransitions>
|
||||||
|
<TransitionCollection>
|
||||||
|
<AddDeleteThemeTransition />
|
||||||
|
</TransitionCollection>
|
||||||
|
</ItemsControl.ItemContainerTransitions>
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="2" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
<!--<controls:CalendarItemControl
|
||||||
x:Name="SingleAllDayEventHolder"
|
x:Name="SingleAllDayEventHolder"
|
||||||
CalendarItem="{x:Bind calendarHelpers:CalendarXamlHelpers.GetFirstAllDayEvent(EventCollection), Mode=OneWay}"
|
CalendarItem="{x:Bind calendarHelpers:CalendarXamlHelpers.GetFirstAllDayEvent(EventCollection), Mode=OneWay}"
|
||||||
Visibility="{x:Bind helpers:XamlHelpers.CountToVisibilityConverter(EventCollection.AllDayEvents.Count), Mode=OneWay}" />
|
Visibility="{x:Bind helpers:XamlHelpers.CountToVisibilityConverter(EventCollection.AllDayEvents.Count), Mode=OneWay}" />
|
||||||
@@ -53,6 +88,6 @@
|
|||||||
</VisualState.StateTriggers>
|
</VisualState.StateTriggers>
|
||||||
</VisualState>
|
</VisualState>
|
||||||
</VisualStateGroup>
|
</VisualStateGroup>
|
||||||
</VisualStateManager.VisualStateGroups>
|
</VisualStateManager.VisualStateGroups>-->
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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.Models.Calendar;
|
||||||
|
|
||||||
|
|
||||||
namespace Wino.Calendar.Controls
|
namespace Wino.Calendar.Controls
|
||||||
@@ -9,36 +9,12 @@ namespace Wino.Calendar.Controls
|
|||||||
{
|
{
|
||||||
#region Dependency Properties
|
#region Dependency Properties
|
||||||
|
|
||||||
public static readonly DependencyProperty EventCollectionProperty = DependencyProperty.Register(nameof(EventCollection), typeof(CalendarEventCollection), typeof(AllDayItemsControl), new PropertyMetadata(null));
|
public static readonly DependencyProperty CalendarDayModelProperty = DependencyProperty.Register(nameof(CalendarDayModel), typeof(CalendarDayModel), typeof(AllDayItemsControl), new PropertyMetadata(null));
|
||||||
public static readonly DependencyProperty AllDayEventTemplateProperty = DependencyProperty.Register(nameof(AllDayEventTemplate), typeof(DataTemplate), typeof(AllDayItemsControl), new PropertyMetadata(null));
|
|
||||||
public static readonly DependencyProperty RegularEventItemTemplateProperty = DependencyProperty.Register(nameof(RegularEventItemTemplate), typeof(DataTemplate), typeof(AllDayItemsControl), new PropertyMetadata(null));
|
|
||||||
|
|
||||||
/// <summary>
|
public CalendarDayModel CalendarDayModel
|
||||||
/// Item template for all-day events to display in summary view area.
|
|
||||||
/// More than 2 events will be shown in Flyout.
|
|
||||||
/// </summary>
|
|
||||||
public DataTemplate AllDayEventTemplate
|
|
||||||
{
|
{
|
||||||
get { return (DataTemplate)GetValue(AllDayEventTemplateProperty); }
|
get { return (CalendarDayModel)GetValue(CalendarDayModelProperty); }
|
||||||
set { SetValue(AllDayEventTemplateProperty, value); }
|
set { SetValue(CalendarDayModelProperty, value); }
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Item template for all-day events to display in summary view's Flyout.
|
|
||||||
/// </summary>
|
|
||||||
public DataTemplate RegularEventItemTemplate
|
|
||||||
{
|
|
||||||
get { return (DataTemplate)GetValue(RegularEventItemTemplateProperty); }
|
|
||||||
set { SetValue(RegularEventItemTemplateProperty, value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whole collection of events to display.
|
|
||||||
/// </summary>
|
|
||||||
public CalendarEventCollection EventCollection
|
|
||||||
{
|
|
||||||
get { return (CalendarEventCollection)GetValue(EventCollectionProperty); }
|
|
||||||
set { SetValue(EventCollectionProperty, value); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
x:Class="Wino.Calendar.Controls.CalendarItemControl"
|
x:Class="Wino.Calendar.Controls.CalendarItemControl"
|
||||||
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.Core.UWP.Controls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:helpers="using:Wino.Helpers"
|
xmlns:helpers="using:Wino.Helpers"
|
||||||
xmlns:local="using:Wino.Calendar.Controls"
|
xmlns:local="using:Wino.Calendar.Controls"
|
||||||
@@ -18,7 +19,13 @@
|
|||||||
CornerRadius="4"
|
CornerRadius="4"
|
||||||
DoubleTapped="ControlDoubleTapped"
|
DoubleTapped="ControlDoubleTapped"
|
||||||
RightTapped="ControlRightTapped"
|
RightTapped="ControlRightTapped"
|
||||||
Tapped="ControlTapped">
|
Tapped="ControlTapped"
|
||||||
|
ToolTipService.ToolTip="{x:Bind CalendarItemTitle, Mode=OneWay}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Grid.ContextFlyout>
|
<Grid.ContextFlyout>
|
||||||
<MenuFlyout Opened="ContextFlyoutOpened">
|
<MenuFlyout Opened="ContextFlyoutOpened">
|
||||||
<MenuFlyoutItem Text="as" />
|
<MenuFlyoutItem Text="as" />
|
||||||
@@ -26,33 +33,49 @@
|
|||||||
<MenuFlyoutItem Text="as" />
|
<MenuFlyoutItem Text="as" />
|
||||||
</MenuFlyout>
|
</MenuFlyout>
|
||||||
</Grid.ContextFlyout>
|
</Grid.ContextFlyout>
|
||||||
<Grid x:Name="MainBackground" Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}" />
|
|
||||||
|
<Grid
|
||||||
|
x:Name="MainBackground"
|
||||||
|
Grid.ColumnSpan="2"
|
||||||
|
Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}" />
|
||||||
|
|
||||||
<Rectangle
|
<Rectangle
|
||||||
x:Name="MainBorder"
|
x:Name="MainBorder"
|
||||||
|
Grid.ColumnSpan="2"
|
||||||
Stroke="{ThemeResource CalendarItemBorderBrush}"
|
Stroke="{ThemeResource CalendarItemBorderBrush}"
|
||||||
StrokeThickness="0" />
|
StrokeThickness="0" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
x:Name="EventTitleTextblock"
|
x:Name="EventTitleTextblock"
|
||||||
Margin="2,0"
|
Margin="2,0"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
CharacterSpacing="8"
|
CharacterSpacing="8"
|
||||||
FontSize="13"
|
FontSize="13"
|
||||||
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
|
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
|
||||||
Text="{x:Bind CalendarItem.Title}"
|
HorizontalTextAlignment="Center"
|
||||||
TextWrapping="Wrap" />
|
Text="{x:Bind CalendarItemTitle, Mode=OneWay}"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
|
||||||
<!-- TODO: Event attributes -->
|
<!-- TODO: Event attributes -->
|
||||||
<StackPanel
|
<StackPanel
|
||||||
|
x:Name="AttributeStack"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="0,4,4,0"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<PathIcon
|
<controls:WinoFontIcon
|
||||||
HorizontalAlignment="Center"
|
FontSize="10"
|
||||||
VerticalAlignment="Center"
|
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
|
||||||
Data="F1 M 7.5 3.75 C 6.634114 3.75 5.82194 3.912762 5.063477 4.238281 C 4.305013 4.563803 3.642578 5.009766 3.076172 5.576172 C 2.509766 6.142578 2.063802 6.805014 1.738281 7.563477 C 1.41276 8.32194 1.25 9.134115 1.25 10 C 1.25 10.527344 1.318359 11.056315 1.455078 11.586914 C 1.591797 12.117514 1.79362 12.613933 2.060547 13.076172 C 2.099609 13.147787 2.150065 13.225912 2.211914 13.310547 C 2.273763 13.395183 2.333984 13.483073 2.392578 13.574219 C 2.451172 13.665365 2.501627 13.756511 2.543945 13.847656 C 2.586263 13.938803 2.607422 14.023438 2.607422 14.101562 C 2.607422 14.270834 2.545573 14.417318 2.421875 14.541016 C 2.298177 14.664714 2.151693 14.726562 1.982422 14.726562 C 1.871745 14.726562 1.778971 14.703776 1.704102 14.658203 C 1.629232 14.612631 1.55599 14.550781 1.484375 14.472656 C 1.24349 14.192709 1.030273 13.870443 0.844727 13.505859 C 0.65918 13.141276 0.504557 12.760417 0.380859 12.363281 C 0.257161 11.966146 0.16276 11.564128 0.097656 11.157227 C 0.032552 10.750326 0 10.364584 0 10 C 0 9.316406 0.089518 8.6556 0.268555 8.017578 C 0.447591 7.379558 0.69987 6.782227 1.025391 6.225586 C 1.350911 5.668945 1.741536 5.162761 2.197266 4.707031 C 2.652995 4.251303 3.157552 3.859051 3.710938 3.530273 C 4.264323 3.201498 4.861653 2.947592 5.50293 2.768555 C 6.144206 2.58952 6.809896 2.5 7.5 2.5 L 14.121094 2.5 L 12.685547 1.064453 C 12.561849 0.940756 12.5 0.794271 12.5 0.625 C 12.5 0.45573 12.561849 0.309246 12.685547 0.185547 C 12.809244 0.06185 12.955729 0 13.125 0 C 13.294271 0 13.440755 0.06185 13.564453 0.185547 L 16.064453 2.685547 C 16.18815 2.809246 16.25 2.95573 16.25 3.125 C 16.25 3.294271 16.18815 3.440756 16.064453 3.564453 L 13.564453 6.064453 C 13.440755 6.188151 13.294271 6.25 13.125 6.25 C 12.955729 6.25 12.809244 6.188151 12.685547 6.064453 C 12.561849 5.940756 12.5 5.794271 12.5 5.625 C 12.5 5.455729 12.561849 5.309245 12.685547 5.185547 L 14.121094 3.75 Z M 20 10 C 20 10.690104 19.91048 11.352539 19.731445 11.987305 C 19.552408 12.62207 19.300129 13.217773 18.974609 13.774414 C 18.649088 14.331055 18.258463 14.83724 17.802734 15.292969 C 17.347004 15.748698 16.842447 16.140951 16.289062 16.469727 C 15.735677 16.798502 15.138346 17.052408 14.49707 17.231445 C 13.855793 17.410482 13.190104 17.5 12.5 17.5 L 5.888672 17.5 L 7.314453 18.935547 C 7.43815 19.059244 7.5 19.205729 7.5 19.375 C 7.5 19.544271 7.43815 19.690756 7.314453 19.814453 C 7.190755 19.93815 7.044271 20 6.875 20 C 6.705729 20 6.559244 19.93815 6.435547 19.814453 L 3.935547 17.314453 C 3.811849 17.190756 3.75 17.044271 3.75 16.875 C 3.75 16.705729 3.811849 16.559244 3.935547 16.435547 L 6.435547 13.935547 C 6.559244 13.81185 6.705729 13.75 6.875 13.75 C 7.044271 13.75 7.190755 13.81185 7.314453 13.935547 C 7.43815 14.059245 7.5 14.205729 7.5 14.375 C 7.5 14.544271 7.43815 14.690756 7.314453 14.814453 L 5.888672 16.25 L 12.5 16.25 C 13.365885 16.25 14.178059 16.08724 14.936523 15.761719 C 15.694986 15.436198 16.357422 14.990234 16.923828 14.423828 C 17.490234 13.857422 17.936197 13.194987 18.261719 12.436523 C 18.587238 11.678061 18.75 10.865886 18.75 10 C 18.75 9.628906 18.723957 9.283854 18.671875 8.964844 C 18.619791 8.645834 18.541666 8.336589 18.4375 8.037109 C 18.333332 7.737631 18.204752 7.444662 18.051758 7.158203 C 17.898762 6.871746 17.721354 6.575521 17.519531 6.269531 C 17.473957 6.197917 17.441406 6.139323 17.421875 6.09375 C 17.402344 6.048178 17.392578 5.983074 17.392578 5.898438 C 17.392578 5.729168 17.452799 5.581056 17.573242 5.454102 C 17.693684 5.327149 17.841797 5.263673 18.017578 5.263672 C 18.128254 5.263673 18.221027 5.286459 18.295898 5.332031 C 18.370768 5.377604 18.44401 5.439453 18.515625 5.517578 C 18.75651 5.797527 18.969727 6.119793 19.155273 6.484375 C 19.34082 6.848959 19.495441 7.231445 19.619141 7.631836 C 19.742838 8.032227 19.837238 8.435873 19.902344 8.842773 C 19.967447 9.249675 20 9.635417 20 10 Z "
|
Icon="CalendarEventRepeat"
|
||||||
Visibility="{x:Bind CalendarItem.IsRecurringEvent}" />
|
Visibility="{x:Bind CalendarItem.IsRecurringEvent, Mode=OneWay}" />
|
||||||
|
|
||||||
|
<controls:WinoFontIcon
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
|
||||||
|
Icon="CalendarEventMuiltiDay"
|
||||||
|
Visibility="{x:Bind CalendarItem.IsMultiDayEvent, Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<VisualStateManager.VisualStateGroups>
|
<VisualStateManager.VisualStateGroups>
|
||||||
@@ -78,7 +101,6 @@
|
|||||||
<Setter Target="MainBorder.StrokeThickness" Value="2" />
|
<Setter Target="MainBorder.StrokeThickness" Value="2" />
|
||||||
<Setter Target="MainBorder.Stroke" Value="{ThemeResource CalendarItemDraggingBorderBrush}" />
|
<Setter Target="MainBorder.Stroke" Value="{ThemeResource CalendarItemDraggingBorderBrush}" />
|
||||||
<Setter Target="MainBorder.StrokeDashArray" Value="2.5" />
|
<Setter Target="MainBorder.StrokeDashArray" Value="2.5" />
|
||||||
|
|
||||||
</VisualState.Setters>
|
</VisualState.Setters>
|
||||||
</VisualState>
|
</VisualState>
|
||||||
</VisualStateGroup>
|
</VisualStateGroup>
|
||||||
@@ -86,19 +108,35 @@
|
|||||||
<VisualState x:Name="RegularEvent" />
|
<VisualState x:Name="RegularEvent" />
|
||||||
<VisualState x:Name="AllDayEvent">
|
<VisualState x:Name="AllDayEvent">
|
||||||
<VisualState.Setters>
|
<VisualState.Setters>
|
||||||
<Setter Target="MainGrid.MinHeight" Value="25" />
|
<Setter Target="AttributeStack.VerticalAlignment" Value="Center" />
|
||||||
|
<Setter Target="MainGrid.MinHeight" Value="30" />
|
||||||
|
<Setter Target="MainBorder.StrokeThickness" Value="0.5" />
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
|
||||||
|
<VisualState x:Name="CustomAreaMultiDayEvent">
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Target="MainBackground.Opacity" Value="1" />
|
||||||
|
<Setter Target="MainBorder.StrokeThickness" Value="0.5" />
|
||||||
|
<Setter Target="AttributeStack.Visibility" Value="Collapsed" />
|
||||||
|
<Setter Target="MainGrid.MinHeight" Value="30" />
|
||||||
|
<Setter Target="EventTitleTextblock.HorizontalAlignment" Value="Stretch" />
|
||||||
|
<Setter Target="EventTitleTextblock.HorizontalTextAlignment" Value="Left" />
|
||||||
<Setter Target="EventTitleTextblock.Margin" Value="6,0" />
|
<Setter Target="EventTitleTextblock.Margin" Value="6,0" />
|
||||||
</VisualState.Setters>
|
</VisualState.Setters>
|
||||||
</VisualState>
|
</VisualState>
|
||||||
|
|
||||||
<VisualState x:Name="MultiDayEvent">
|
<VisualState x:Name="MultiDayEvent">
|
||||||
<VisualState.Setters>
|
<VisualState.Setters>
|
||||||
<Setter Target="MainBackground.Opacity" Value="0.6" />
|
<Setter Target="MainBackground.Opacity" Value="0.2" />
|
||||||
<Setter Target="MainGrid.CornerRadius" Value="0" />
|
<Setter Target="MainGrid.IsHitTestVisible" Value="False" />
|
||||||
<Setter Target="MainBorder.StrokeThickness" Value="0.5" />
|
<Setter Target="MainBorder.StrokeThickness" Value="0.5" />
|
||||||
|
<Setter Target="AttributeStack.Visibility" Value="Collapsed" />
|
||||||
|
<Setter Target="EventTitleTextblock.Visibility" Value="Collapsed" />
|
||||||
</VisualState.Setters>
|
</VisualState.Setters>
|
||||||
</VisualState>
|
</VisualState>
|
||||||
</VisualStateGroup>
|
</VisualStateGroup>
|
||||||
</VisualStateManager.VisualStateGroups>
|
</VisualStateManager.VisualStateGroups>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,49 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging;
|
using System.Diagnostics;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Itenso.TimePeriod;
|
||||||
using Windows.UI.Xaml;
|
using Windows.UI.Xaml;
|
||||||
using Windows.UI.Xaml.Controls;
|
using Windows.UI.Xaml.Controls;
|
||||||
using Wino.Calendar.ViewModels.Data;
|
using Wino.Calendar.ViewModels.Data;
|
||||||
using Wino.Calendar.ViewModels.Messages;
|
using Wino.Calendar.ViewModels.Messages;
|
||||||
|
using Wino.Core.Domain;
|
||||||
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
|
|
||||||
namespace Wino.Calendar.Controls
|
namespace Wino.Calendar.Controls
|
||||||
{
|
{
|
||||||
public sealed partial class CalendarItemControl : UserControl
|
public sealed partial class CalendarItemControl : UserControl
|
||||||
{
|
{
|
||||||
|
public bool IsAllDayMultiDayEvent { get; set; }
|
||||||
|
|
||||||
public static readonly DependencyProperty CalendarItemProperty = DependencyProperty.Register(nameof(CalendarItem), typeof(CalendarItemViewModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarItemChanged)));
|
public static readonly DependencyProperty CalendarItemProperty = DependencyProperty.Register(nameof(CalendarItem), typeof(CalendarItemViewModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarItemChanged)));
|
||||||
public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.Register(nameof(IsDragging), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false));
|
public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.Register(nameof(IsDragging), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false));
|
||||||
|
public static readonly DependencyProperty IsCustomEventAreaProperty = DependencyProperty.Register(nameof(IsCustomEventArea), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false));
|
||||||
|
public static readonly DependencyProperty CalendarItemTitleProperty = DependencyProperty.Register(nameof(CalendarItemTitle), typeof(string), typeof(CalendarItemControl), new PropertyMetadata(string.Empty));
|
||||||
|
public static readonly DependencyProperty DisplayingDateProperty = DependencyProperty.Register(nameof(DisplayingDate), typeof(CalendarDayModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnDisplayDateChanged)));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the control is displaying as regular event or all-multi day area in the day control.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCustomEventArea
|
||||||
|
{
|
||||||
|
get { return (bool)GetValue(IsCustomEventAreaProperty); }
|
||||||
|
set { SetValue(IsCustomEventAreaProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Day that the calendar item is rendered at.
|
||||||
|
/// It's needed for title manipulation and some other adjustments later on.
|
||||||
|
/// </summary>
|
||||||
|
public CalendarDayModel DisplayingDate
|
||||||
|
{
|
||||||
|
get { return (CalendarDayModel)GetValue(DisplayingDateProperty); }
|
||||||
|
set { SetValue(DisplayingDateProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CalendarItemTitle
|
||||||
|
{
|
||||||
|
get { return (string)GetValue(CalendarItemTitleProperty); }
|
||||||
|
set { SetValue(CalendarItemTitleProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
public CalendarItemViewModel CalendarItem
|
public CalendarItemViewModel CalendarItem
|
||||||
{
|
{
|
||||||
@@ -28,14 +62,77 @@ namespace Wino.Calendar.Controls
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void OnDisplayDateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (d is CalendarItemControl control)
|
||||||
|
{
|
||||||
|
control.UpdateControlVisuals();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void OnCalendarItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
private static void OnCalendarItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (d is CalendarItemControl control)
|
if (d is CalendarItemControl control)
|
||||||
{
|
{
|
||||||
control.UpdateVisualStates();
|
control.UpdateControlVisuals();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateControlVisuals()
|
||||||
|
{
|
||||||
|
// Depending on the calendar item's duration and attributes, we might need to change the display title.
|
||||||
|
// 1. Multi-Day events should display the start date and end date.
|
||||||
|
// 2. Multi-Day events that occupy the whole day just shows 'all day'.
|
||||||
|
// 3. Other events should display the title.
|
||||||
|
|
||||||
|
if (CalendarItem == null) return;
|
||||||
|
if (DisplayingDate == null) return;
|
||||||
|
|
||||||
|
if (CalendarItem.IsMultiDayEvent)
|
||||||
|
{
|
||||||
|
// Multi day events are divided into 3 categories:
|
||||||
|
// 1. All day events
|
||||||
|
// 2. Events that started after the period.
|
||||||
|
// 3. Events that started before the period and finishes within the period.
|
||||||
|
|
||||||
|
var periodRelation = CalendarItem.Period.GetRelation(DisplayingDate.Period);
|
||||||
|
|
||||||
|
if (periodRelation == Itenso.TimePeriod.PeriodRelation.StartInside ||
|
||||||
|
periodRelation == PeriodRelation.EnclosingStartTouching)
|
||||||
|
{
|
||||||
|
// hour -> title
|
||||||
|
CalendarItemTitle = $"{DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.StartDate.TimeOfDay)} -> {CalendarItem.Title}";
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
periodRelation == PeriodRelation.EndInside ||
|
||||||
|
periodRelation == PeriodRelation.EnclosingEndTouching)
|
||||||
|
{
|
||||||
|
// title <- hour
|
||||||
|
CalendarItemTitle = $"{CalendarItem.Title} <- {DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.EndDate.TimeOfDay)}";
|
||||||
|
}
|
||||||
|
else if (periodRelation == PeriodRelation.Enclosing)
|
||||||
|
{
|
||||||
|
// This event goes all day and it's multi-day.
|
||||||
|
// Item must be hidden in the calendar but displayed on the custom area at the top.
|
||||||
|
|
||||||
|
CalendarItemTitle = $"{Translator.CalendarItemAllDay} {CalendarItem.Title}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Not expected, but there it is.
|
||||||
|
CalendarItemTitle = CalendarItem.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"{CalendarItem.Title} Period relation with {DisplayingDate.Period.ToString()}: {periodRelation}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CalendarItemTitle = CalendarItem.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateVisualStates();
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateVisualStates()
|
private void UpdateVisualStates()
|
||||||
{
|
{
|
||||||
if (CalendarItem == null) return;
|
if (CalendarItem == null) return;
|
||||||
@@ -46,7 +143,15 @@ namespace Wino.Calendar.Controls
|
|||||||
}
|
}
|
||||||
else if (CalendarItem.IsMultiDayEvent)
|
else if (CalendarItem.IsMultiDayEvent)
|
||||||
{
|
{
|
||||||
VisualStateManager.GoToState(this, "MultiDayEvent", true);
|
if (IsCustomEventArea)
|
||||||
|
{
|
||||||
|
VisualStateManager.GoToState(this, "CustomAreaMultiDayEvent", true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Hide it.
|
||||||
|
VisualStateManager.GoToState(this, "MultiDayEvent", true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ namespace Wino.Calendar.Controls
|
|||||||
private const string PART_IsTodayBorder = nameof(PART_IsTodayBorder);
|
private const string PART_IsTodayBorder = nameof(PART_IsTodayBorder);
|
||||||
private const string PART_ColumnHeaderText = nameof(PART_ColumnHeaderText);
|
private const string PART_ColumnHeaderText = nameof(PART_ColumnHeaderText);
|
||||||
|
|
||||||
|
private const string PART_AllDayItemsControl = nameof(PART_AllDayItemsControl);
|
||||||
|
|
||||||
private const string TodayState = nameof(TodayState);
|
private const string TodayState = nameof(TodayState);
|
||||||
private const string NotTodayState = nameof(NotTodayState);
|
private const string NotTodayState = nameof(NotTodayState);
|
||||||
|
|
||||||
private TextBlock HeaderDateDayText;
|
private TextBlock HeaderDateDayText;
|
||||||
private TextBlock ColumnHeaderText;
|
private TextBlock ColumnHeaderText;
|
||||||
private Border IsTodayBorder;
|
private Border IsTodayBorder;
|
||||||
|
private AllDayItemsControl AllDayItemsControl;
|
||||||
|
|
||||||
public CalendarDayModel DayModel
|
public CalendarDayModel DayModel
|
||||||
{
|
{
|
||||||
@@ -38,6 +41,7 @@ namespace Wino.Calendar.Controls
|
|||||||
HeaderDateDayText = GetTemplateChild(PART_HeaderDateDayText) as TextBlock;
|
HeaderDateDayText = GetTemplateChild(PART_HeaderDateDayText) as TextBlock;
|
||||||
ColumnHeaderText = GetTemplateChild(PART_ColumnHeaderText) as TextBlock;
|
ColumnHeaderText = GetTemplateChild(PART_ColumnHeaderText) as TextBlock;
|
||||||
IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border;
|
IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border;
|
||||||
|
AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as AllDayItemsControl;
|
||||||
|
|
||||||
UpdateValues();
|
UpdateValues();
|
||||||
}
|
}
|
||||||
@@ -57,6 +61,8 @@ namespace Wino.Calendar.Controls
|
|||||||
HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString();
|
HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString();
|
||||||
ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo);
|
ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo);
|
||||||
|
|
||||||
|
AllDayItemsControl.CalendarDayModel = DayModel;
|
||||||
|
|
||||||
bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date;
|
bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date;
|
||||||
|
|
||||||
VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false);
|
VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Specialized;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CommunityToolkit.WinUI;
|
using CommunityToolkit.WinUI;
|
||||||
using Itenso.TimePeriod;
|
using Itenso.TimePeriod;
|
||||||
@@ -19,13 +19,12 @@ namespace Wino.Calendar.Controls
|
|||||||
private const double LastItemRightExtraMargin = 12d;
|
private const double LastItemRightExtraMargin = 12d;
|
||||||
|
|
||||||
// Store each ICalendarItem measurements by their Id.
|
// Store each ICalendarItem measurements by their Id.
|
||||||
private readonly Dictionary<Guid, CalendarItemMeasurement> _measurements = new Dictionary<Guid, CalendarItemMeasurement>();
|
private readonly Dictionary<ICalendarItem, CalendarItemMeasurement> _measurements = new Dictionary<ICalendarItem, CalendarItemMeasurement>();
|
||||||
|
|
||||||
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 static readonly DependencyProperty PeriodProperty = DependencyProperty.Register(nameof(Period), typeof(ITimePeriod), typeof(WinoCalendarPanel), new PropertyMetadata(null));
|
||||||
|
|
||||||
|
|
||||||
public ITimePeriod Period
|
public ITimePeriod Period
|
||||||
{
|
{
|
||||||
get { return (ITimePeriod)GetValue(PeriodProperty); }
|
get { return (ITimePeriod)GetValue(PeriodProperty); }
|
||||||
@@ -46,9 +45,6 @@ namespace Wino.Calendar.Controls
|
|||||||
|
|
||||||
private void ResetMeasurements() => _measurements.Clear();
|
private void ResetMeasurements() => _measurements.Clear();
|
||||||
|
|
||||||
// No need to handle actions. Each action requires a full measurement update.
|
|
||||||
private void EventCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => ResetMeasurements();
|
|
||||||
|
|
||||||
private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight)
|
private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight)
|
||||||
{
|
{
|
||||||
var childStart = calendarItemViewModel.StartDate;
|
var childStart = calendarItemViewModel.StartDate;
|
||||||
@@ -75,31 +71,25 @@ namespace Wino.Calendar.Controls
|
|||||||
|
|
||||||
private double GetChildHeight(ICalendarItem child)
|
private double GetChildHeight(ICalendarItem child)
|
||||||
{
|
{
|
||||||
double childDurationInMinutes = 0d;
|
// All day events are not measured.
|
||||||
|
if (child.IsAllDayEvent) return 0;
|
||||||
|
|
||||||
|
double childDurationInMinutes = 0d;
|
||||||
double availableHeight = HourHeight * 24;
|
double availableHeight = HourHeight * 24;
|
||||||
|
|
||||||
var childStart = child.Period.Start;
|
var periodRelation = child.Period.GetRelation(Period);
|
||||||
var childEnd = child.Period.End;
|
|
||||||
|
|
||||||
// Multi-day event.
|
Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}");
|
||||||
|
|
||||||
if (childStart < Period.Start)
|
if (!child.IsMultiDayEvent)
|
||||||
{
|
{
|
||||||
if (childEnd >= Period.End)
|
childDurationInMinutes = child.Period.Duration.TotalMinutes;
|
||||||
{
|
|
||||||
// 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
|
else
|
||||||
{
|
{
|
||||||
childDurationInMinutes = (childEnd - childStart).TotalMinutes;
|
// Multi-day event.
|
||||||
|
// Check how many of the event falls into the current period.
|
||||||
|
childDurationInMinutes = (child.Period.End - Period.Start).TotalMinutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (childDurationInMinutes / 1440) * availableHeight;
|
return (childDurationInMinutes / 1440) * availableHeight;
|
||||||
@@ -131,33 +121,51 @@ namespace Wino.Calendar.Controls
|
|||||||
|
|
||||||
if (!calendarControls.Any()) return base.ArrangeOverride(finalSize);
|
if (!calendarControls.Any()) return base.ArrangeOverride(finalSize);
|
||||||
|
|
||||||
if (_measurements.Count == 0 && calendarControls.Any())
|
var events = calendarControls.Select(a => a.Content as CalendarItemViewModel);
|
||||||
{
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
var events = calendarControls.Select(a => a.Content as CalendarItemViewModel);
|
LayoutEvents(events);
|
||||||
|
|
||||||
LayoutEvents(events);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var control in calendarControls)
|
foreach (var control in calendarControls)
|
||||||
{
|
{
|
||||||
// We can't arrange this child. It doesn't have a valid ICalendarItem or measurement.
|
// We can't arrange this child.
|
||||||
if (!(control.Content is ICalendarItem child) || !_measurements.ContainsKey(child.Id)) continue;
|
if (!(control.Content is ICalendarItem child)) continue;
|
||||||
|
|
||||||
var childMeasurement = _measurements[child.Id];
|
bool isHorizontallyLastItem = false;
|
||||||
|
|
||||||
double childHeight = Math.Max(0, GetChildHeight(child));
|
double childWidth = 0,
|
||||||
double childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width));
|
childHeight = Math.Max(0, GetChildHeight(child)),
|
||||||
double childTop = Math.Max(0, GetChildTopMargin(child, availableHeight));
|
childTop = Math.Max(0, GetChildTopMargin(child, availableHeight)),
|
||||||
double childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth));
|
childLeft = 0;
|
||||||
|
|
||||||
bool isHorizontallyLastItem = childMeasurement.Right == 1;
|
// No need to measure anything here.
|
||||||
|
if (childHeight == 0) continue;
|
||||||
|
|
||||||
|
if (!_measurements.ContainsKey(child))
|
||||||
|
{
|
||||||
|
// Multi-day event.
|
||||||
|
|
||||||
|
childLeft = 0;
|
||||||
|
childWidth = availableWidth;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var childMeasurement = _measurements[child];
|
||||||
|
|
||||||
|
childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width));
|
||||||
|
childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth));
|
||||||
|
|
||||||
|
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.
|
double extraRightMargin = 0;
|
||||||
var extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0;
|
|
||||||
|
// Multi-day events don't have any margin and their hit test is disabled.
|
||||||
|
if (!child.IsMultiDayEvent)
|
||||||
|
{
|
||||||
|
// Max of 5% of the width or 20px max.
|
||||||
|
extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (childWidth < 0) childWidth = 1;
|
if (childWidth < 0) childWidth = 1;
|
||||||
|
|
||||||
@@ -178,13 +186,13 @@ namespace Wino.Calendar.Controls
|
|||||||
|
|
||||||
private void AddOrUpdateMeasurement(ICalendarItem calendarItem, CalendarItemMeasurement measurement)
|
private void AddOrUpdateMeasurement(ICalendarItem calendarItem, CalendarItemMeasurement measurement)
|
||||||
{
|
{
|
||||||
if (_measurements.ContainsKey(calendarItem.Id))
|
if (_measurements.ContainsKey(calendarItem))
|
||||||
{
|
{
|
||||||
_measurements[calendarItem.Id] = measurement;
|
_measurements[calendarItem] = measurement;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_measurements.Add(calendarItem.Id, measurement);
|
_measurements.Add(calendarItem, measurement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +204,9 @@ namespace Wino.Calendar.Controls
|
|||||||
|
|
||||||
foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate))
|
foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate))
|
||||||
{
|
{
|
||||||
|
// Multi-day events are not measured.
|
||||||
|
if (ev.IsMultiDayEvent) continue;
|
||||||
|
|
||||||
if (ev.Period.Start >= lastEventEnding)
|
if (ev.Period.Start >= lastEventEnding)
|
||||||
{
|
{
|
||||||
PackEvents(columns);
|
PackEvents(columns);
|
||||||
|
|||||||
22
Wino.Calendar/Selectors/CustomAreaCalendarItemSelector.cs
Normal file
22
Wino.Calendar/Selectors/CustomAreaCalendarItemSelector.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using Windows.UI.Xaml;
|
||||||
|
using Windows.UI.Xaml.Controls;
|
||||||
|
using Wino.Calendar.ViewModels.Data;
|
||||||
|
|
||||||
|
namespace Wino.Calendar.Selectors
|
||||||
|
{
|
||||||
|
public class CustomAreaCalendarItemSelector : DataTemplateSelector
|
||||||
|
{
|
||||||
|
public DataTemplate AllDayTemplate { get; set; }
|
||||||
|
public DataTemplate MultiDayTemplate { get; set; }
|
||||||
|
|
||||||
|
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
|
||||||
|
{
|
||||||
|
if (item is CalendarItemViewModel calendarItemViewModel)
|
||||||
|
{
|
||||||
|
return calendarItemViewModel.IsMultiDayEvent ? MultiDayTemplate : AllDayTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.SelectTemplateCore(item, container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,15 +11,7 @@
|
|||||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||||
xmlns:toolkitControls="using:CommunityToolkit.WinUI.Controls">
|
xmlns:toolkitControls="using:CommunityToolkit.WinUI.Controls">
|
||||||
|
|
||||||
<!-- Default Calendar Item View Model Template -->
|
|
||||||
<DataTemplate x:Key="CalendarItemViewModelItemTemplate" x:DataType="data:CalendarItemViewModel">
|
|
||||||
<controls:CalendarItemControl CalendarItem="{x:Bind}" />
|
|
||||||
</DataTemplate>
|
|
||||||
|
|
||||||
<!-- All-Day Event template -->
|
|
||||||
<DataTemplate x:Key="AllDayEventItemTemplate" x:DataType="data:CalendarItemViewModel">
|
|
||||||
<TextBlock Text="{x:Bind Title}" />
|
|
||||||
</DataTemplate>
|
|
||||||
|
|
||||||
<!-- 08:00 or 8 AM/PM on the left etc. -->
|
<!-- 08:00 or 8 AM/PM on the left etc. -->
|
||||||
<DataTemplate x:Key="DayCalendarHourHeaderTemplate" x:DataType="models:DayHeaderRenderModel">
|
<DataTemplate x:Key="DayCalendarHourHeaderTemplate" x:DataType="models:DayHeaderRenderModel">
|
||||||
@@ -33,7 +25,16 @@
|
|||||||
|
|
||||||
<!-- Vertical panel that renders items on canvas. -->
|
<!-- Vertical panel that renders items on canvas. -->
|
||||||
<DataTemplate x:Key="DayCalendarItemVerticalRenderTemplate" x:DataType="models:CalendarDayModel">
|
<DataTemplate x:Key="DayCalendarItemVerticalRenderTemplate" x:DataType="models:CalendarDayModel">
|
||||||
<ItemsControl ItemTemplate="{StaticResource CalendarItemViewModelItemTemplate}" ItemsSource="{x:Bind EventsCollection.RegularEvents}">
|
<ItemsControl x:Name="RegularEventItemsControl" ItemsSource="{x:Bind EventsCollection.RegularEvents}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<!-- Default Calendar Item View Model Template -->
|
||||||
|
<DataTemplate x:DataType="data:CalendarItemViewModel">
|
||||||
|
<controls:CalendarItemControl
|
||||||
|
CalendarItem="{x:Bind}"
|
||||||
|
DisplayingDate="{Binding ElementName=RegularEventItemsControl, Path=DataContext}"
|
||||||
|
IsCustomEventArea="False" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
<ItemsControl.ItemContainerTransitions>
|
<ItemsControl.ItemContainerTransitions>
|
||||||
<TransitionCollection>
|
<TransitionCollection>
|
||||||
<PaneThemeTransition Edge="Left" />
|
<PaneThemeTransition Edge="Left" />
|
||||||
@@ -65,10 +66,7 @@
|
|||||||
ItemsSource="{x:Bind CalendarDays}">
|
ItemsSource="{x:Bind CalendarDays}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="models:CalendarDayModel">
|
<DataTemplate x:DataType="models:CalendarDayModel">
|
||||||
<controls:DayColumnControl
|
<controls:DayColumnControl DayModel="{x:Bind}" />
|
||||||
MinHeight="100"
|
|
||||||
MaxHeight="200"
|
|
||||||
DayModel="{x:Bind}" />
|
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
@@ -212,7 +210,7 @@
|
|||||||
|
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" MinHeight="35" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Day number -->
|
<!-- Day number -->
|
||||||
@@ -221,15 +219,12 @@
|
|||||||
<!-- Extras -->
|
<!-- Extras -->
|
||||||
<StackPanel Grid.Column="1" HorizontalAlignment="Right" />
|
<StackPanel Grid.Column="1" HorizontalAlignment="Right" />
|
||||||
|
|
||||||
<!-- All-Day Events -->
|
<!-- All-Multi Day Events -->
|
||||||
|
|
||||||
<!-- TODO: This control leaks. -->
|
|
||||||
<controls:AllDayItemsControl
|
<controls:AllDayItemsControl
|
||||||
|
x:Name="PART_AllDayItemsControl"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.ColumnSpan="2"
|
Grid.ColumnSpan="2"
|
||||||
AllDayEventTemplate="{StaticResource CalendarItemViewModelItemTemplate}"
|
Margin="0,6" />
|
||||||
EventCollection="{Binding EventsCollection}"
|
|
||||||
RegularEventItemTemplate="{StaticResource CalendarItemViewModelItemTemplate}" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<VisualStateManager.VisualStateGroups>
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -158,6 +158,7 @@
|
|||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Models\CalendarItemMeasurement.cs" />
|
<Compile Include="Models\CalendarItemMeasurement.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="Selectors\CustomAreaCalendarItemSelector.cs" />
|
||||||
<Compile Include="Services\AccountCalendarStateService.cs" />
|
<Compile Include="Services\AccountCalendarStateService.cs" />
|
||||||
<Compile Include="Services\CalendarAuthenticatorConfig.cs" />
|
<Compile Include="Services\CalendarAuthenticatorConfig.cs" />
|
||||||
<Compile Include="Services\DialogService.cs" />
|
<Compile Include="Services\DialogService.cs" />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using Itenso.TimePeriod;
|
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;
|
||||||
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Collections
|
namespace Wino.Core.Domain.Collections
|
||||||
{
|
{
|
||||||
@@ -19,14 +20,16 @@ namespace Wino.Core.Domain.Collections
|
|||||||
private ObservableRangeCollection<ICalendarItem> _internalAllDayEvents = [];
|
private ObservableRangeCollection<ICalendarItem> _internalAllDayEvents = [];
|
||||||
|
|
||||||
public ReadOnlyObservableCollection<ICalendarItem> RegularEvents { get; }
|
public ReadOnlyObservableCollection<ICalendarItem> RegularEvents { get; }
|
||||||
public ReadOnlyObservableCollection<ICalendarItem> AllDayEvents { get; }
|
public ReadOnlyObservableCollection<ICalendarItem> AllDayEvents { get; } // TODO: Rename this to include multi-day events.
|
||||||
public ITimePeriod Period { get; }
|
public ITimePeriod Period { get; }
|
||||||
|
public CalendarSettings Settings { get; }
|
||||||
|
|
||||||
private readonly List<ICalendarItem> _allItems = new List<ICalendarItem>();
|
private readonly List<ICalendarItem> _allItems = new List<ICalendarItem>();
|
||||||
|
|
||||||
public CalendarEventCollection(ITimePeriod period)
|
public CalendarEventCollection(ITimePeriod period, CalendarSettings settings)
|
||||||
{
|
{
|
||||||
Period = period;
|
Period = period;
|
||||||
|
Settings = settings;
|
||||||
|
|
||||||
RegularEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalRegularEvents);
|
RegularEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalRegularEvents);
|
||||||
AllDayEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalAllDayEvents);
|
AllDayEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalAllDayEvents);
|
||||||
@@ -44,40 +47,67 @@ namespace Wino.Core.Domain.Collections
|
|||||||
{
|
{
|
||||||
foreach (var item in _allItems)
|
foreach (var item in _allItems)
|
||||||
{
|
{
|
||||||
var collection = GetProperCollectionForCalendarItem(item);
|
var collections = GetProperCollectionsForCalendarItem(item);
|
||||||
|
|
||||||
if (!visibleCalendarIds.Contains(item.AssignedCalendar.Id) && collection.Contains(item))
|
foreach (var collection in collections)
|
||||||
{
|
{
|
||||||
RemoveCalendarItemInternal(collection, item, false);
|
if (!visibleCalendarIds.Contains(item.AssignedCalendar.Id) && collection.Contains(item))
|
||||||
}
|
{
|
||||||
else if (visibleCalendarIds.Contains(item.AssignedCalendar.Id) && !collection.Contains(item))
|
RemoveCalendarItemInternal(collection, item, false);
|
||||||
{
|
}
|
||||||
AddCalendarItemInternal(collection, item, false);
|
else if (visibleCalendarIds.Contains(item.AssignedCalendar.Id) && !collection.Contains(item))
|
||||||
|
{
|
||||||
|
AddCalendarItemInternal(collection, item, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ObservableRangeCollection<ICalendarItem> GetProperCollectionForCalendarItem(ICalendarItem calendarItem)
|
private IEnumerable<ObservableRangeCollection<ICalendarItem>> GetProperCollectionsForCalendarItem(ICalendarItem calendarItem)
|
||||||
{
|
{
|
||||||
// Event duration is not simply enough to determine whether it's an all-day event or not.
|
// All-day events go to all days.
|
||||||
// Event may start at 11:00 PM and end next day at 11:00 PM. It's not an all-day event.
|
// Multi-day events go to both.
|
||||||
// It's a multi-day event.
|
// Anything else goes to regular.
|
||||||
|
|
||||||
bool isAllDayEvent = calendarItem.Period.Duration.TotalDays == 1 && calendarItem.Period.Start.TimeOfDay == TimeSpan.Zero;
|
if (calendarItem.IsAllDayEvent)
|
||||||
|
{
|
||||||
return isAllDayEvent ? _internalAllDayEvents : _internalRegularEvents;
|
return [_internalAllDayEvents];
|
||||||
|
}
|
||||||
|
else if (calendarItem.IsMultiDayEvent)
|
||||||
|
{
|
||||||
|
if (Settings.GhostRenderAllDayItems)
|
||||||
|
{
|
||||||
|
return [_internalRegularEvents, _internalAllDayEvents];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return [_internalAllDayEvents];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return [_internalRegularEvents];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddCalendarItem(ICalendarItem calendarItem)
|
public void AddCalendarItem(ICalendarItem calendarItem)
|
||||||
{
|
{
|
||||||
var collection = GetProperCollectionForCalendarItem(calendarItem);
|
var collections = GetProperCollectionsForCalendarItem(calendarItem);
|
||||||
AddCalendarItemInternal(collection, calendarItem);
|
|
||||||
|
foreach (var collection in collections)
|
||||||
|
{
|
||||||
|
AddCalendarItemInternal(collection, calendarItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveCalendarItem(ICalendarItem calendarItem)
|
public void RemoveCalendarItem(ICalendarItem calendarItem)
|
||||||
{
|
{
|
||||||
var collection = GetProperCollectionForCalendarItem(calendarItem);
|
var collections = GetProperCollectionsForCalendarItem(calendarItem);
|
||||||
RemoveCalendarItemInternal(collection, calendarItem);
|
|
||||||
|
foreach (var collection in collections)
|
||||||
|
{
|
||||||
|
RemoveCalendarItemInternal(collection, calendarItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool create = true)
|
private void AddCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool create = true)
|
||||||
|
|||||||
42
Wino.Core.Domain/Collections/DayRangeCollection.cs
Normal file
42
Wino.Core.Domain/Collections/DayRangeCollection.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Collections
|
||||||
|
{
|
||||||
|
public class DayRangeCollection : ObservableRangeCollection<DayRangeRenderModel>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the range of dates that are currently displayed in the collection.
|
||||||
|
/// </summary>
|
||||||
|
public DateRange DisplayRange
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Count == 0) return null;
|
||||||
|
|
||||||
|
var minimumLoadedDate = this[0].CalendarRenderOptions.DateRange.StartDate;
|
||||||
|
var maximumLoadedDate = this[Count - 1].CalendarRenderOptions.DateRange.EndDate;
|
||||||
|
|
||||||
|
return new DateRange(minimumLoadedDate, maximumLoadedDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveCalendarItem(ICalendarItem calendarItem)
|
||||||
|
{
|
||||||
|
foreach (var dayRange in this)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddCalendarItem(ICalendarItem calendarItem)
|
||||||
|
{
|
||||||
|
foreach (var dayRange in this)
|
||||||
|
{
|
||||||
|
var calendarDayModel = dayRange.CalendarDays.FirstOrDefault(x => x.Period.HasInside(calendarItem.Period.Start));
|
||||||
|
calendarDayModel?.EventsCollection.AddCalendarItem(calendarItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ namespace Wino.Core.Domain.Entities.Calendar
|
|||||||
{
|
{
|
||||||
[PrimaryKey]
|
[PrimaryKey]
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
public string RemoteEventId { get; set; }
|
||||||
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; }
|
||||||
@@ -38,25 +39,51 @@ namespace Wino.Core.Domain.Entities.Calendar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Events that starts at midnight and ends at midnight are considered all-day events.
|
||||||
|
/// </summary>
|
||||||
public bool IsAllDayEvent
|
public bool IsAllDayEvent
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return StartDate.TimeOfDay == TimeSpan.Zero && EndDate.TimeOfDay == TimeSpan.Zero;
|
return
|
||||||
|
StartDate.TimeOfDay == TimeSpan.Zero &&
|
||||||
|
EndDate.TimeOfDay == TimeSpan.Zero;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Events that are not all-day events and last more than one day are considered multi-day events.
|
||||||
|
/// </summary>
|
||||||
public bool IsMultiDayEvent
|
public bool IsMultiDayEvent
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return StartDate.Date != EndDate.Date;
|
return Period.Duration.TotalDays >= 1 && !IsAllDayEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public double DurationInSeconds { get; set; }
|
public double DurationInSeconds { get; set; }
|
||||||
public string Recurrence { get; set; }
|
public string Recurrence { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The id of the parent calendar item of the recurring event.
|
||||||
|
/// Exceptional instances are stored as a separate calendar item.
|
||||||
|
/// This makes the calendar item a child of the recurring event.s
|
||||||
|
/// </summary>
|
||||||
|
public Guid? RecurringCalendarItemId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates read-only events. Default is false.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsLocked { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hidden events must not be displayed to the user.
|
||||||
|
/// This usually happens when a child instance of recurring parent hapens.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsHidden { get; set; }
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
public string CustomEventColorHex { get; set; }
|
public string CustomEventColorHex { get; set; }
|
||||||
public string HtmlLink { get; set; }
|
public string HtmlLink { get; set; }
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
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, DayRangeRenderModel dayRangeRenderModel);
|
Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, DayRangeRenderModel dayRangeRenderModel);
|
||||||
|
Task<CalendarItem> GetCalendarItemAsync(Guid accountCalendarId, string remoteEventId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
DayOfWeek WorkingDayStart { get; set; }
|
DayOfWeek WorkingDayStart { get; set; }
|
||||||
DayOfWeek WorkingDayEnd { get; set; }
|
DayOfWeek WorkingDayEnd { get; set; }
|
||||||
double HourHeight { get; set; }
|
double HourHeight { get; set; }
|
||||||
|
bool GhostRenderAllDayEvents { get; set; }
|
||||||
|
|
||||||
CalendarSettings GetCurrentCalendarSettings();
|
CalendarSettings GetCurrentCalendarSettings();
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace Wino.Core.Domain.Models.Calendar
|
|||||||
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);
|
EventsCollection = new CalendarEventCollection(Period, calendarRenderOptions.CalendarSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime RepresentingDate { get; }
|
public DateTime RepresentingDate { get; }
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ namespace Wino.Core.Domain.Models.Calendar
|
|||||||
TimeSpan WorkingHourEnd,
|
TimeSpan WorkingHourEnd,
|
||||||
double HourHeight,
|
double HourHeight,
|
||||||
DayHeaderDisplayType DayHeaderDisplayType,
|
DayHeaderDisplayType DayHeaderDisplayType,
|
||||||
CultureInfo CultureInfo)
|
CultureInfo CultureInfo,
|
||||||
|
bool GhostRenderAllDayItems)
|
||||||
{
|
{
|
||||||
public TimeSpan? GetTimeSpan(string selectedTime)
|
public TimeSpan? GetTimeSpan(string selectedTime)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Itenso.TimePeriod;
|
using Itenso.TimePeriod;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Calendar
|
namespace Wino.Core.Domain.Models.Calendar
|
||||||
{
|
{
|
||||||
@@ -13,8 +11,6 @@ namespace Wino.Core.Domain.Models.Calendar
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class DayRangeRenderModel
|
public class DayRangeRenderModel
|
||||||
{
|
{
|
||||||
public event EventHandler<CalendarDayModel> CalendarDayEventCollectionUpdated;
|
|
||||||
|
|
||||||
public ITimePeriod Period { get; }
|
public ITimePeriod Period { get; }
|
||||||
public List<CalendarDayModel> CalendarDays { get; } = [];
|
public List<CalendarDayModel> CalendarDays { get; } = [];
|
||||||
public List<DayHeaderRenderModel> DayHeaders { get; } = [];
|
public List<DayHeaderRenderModel> DayHeaders { get; } = [];
|
||||||
@@ -29,8 +25,6 @@ namespace Wino.Core.Domain.Models.Calendar
|
|||||||
var representingDate = calendarRenderOptions.DateRange.StartDate.AddDays(i);
|
var representingDate = calendarRenderOptions.DateRange.StartDate.AddDays(i);
|
||||||
var calendarDayModel = new CalendarDayModel(representingDate, calendarRenderOptions);
|
var calendarDayModel = new CalendarDayModel(representingDate, calendarRenderOptions);
|
||||||
|
|
||||||
RegisterCalendarDayEvents(calendarDayModel);
|
|
||||||
|
|
||||||
CalendarDays.Add(calendarDayModel);
|
CalendarDays.Add(calendarDayModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,29 +47,11 @@ namespace Wino.Core.Domain.Models.Calendar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterCalendarDayEvents(CalendarDayModel calendarDayModel)
|
|
||||||
{
|
|
||||||
calendarDayModel.EventsCollection.CalendarItemAdded += CalendarItemAdded;
|
|
||||||
calendarDayModel.EventsCollection.CalendarItemRemoved += CalendarItemRemoved;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: These handlers have incorrect senders. They should be the CalendarDayModel.
|
//public void AddEvent(ICalendarItem calendarEventModel)
|
||||||
private void CalendarItemRemoved(object sender, ICalendarItem e)
|
//{
|
||||||
=> CalendarDayEventCollectionUpdated?.Invoke(this, sender as CalendarDayModel);
|
// var calendarDayModel = CalendarDays.FirstOrDefault(x => x.Period.HasInside(calendarEventModel.Period.Start));
|
||||||
|
// calendarDayModel?.EventsCollection.AddCalendarItem(calendarEventModel);
|
||||||
private void CalendarItemAdded(object sender, ICalendarItem e)
|
//}
|
||||||
=> CalendarDayEventCollectionUpdated?.Invoke(this, sender as CalendarDayModel);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unregisters all calendar item change listeners to draw the UI for calendar events.
|
|
||||||
/// </summary>
|
|
||||||
public void UnregisterAll()
|
|
||||||
{
|
|
||||||
foreach (var day in CalendarDays)
|
|
||||||
{
|
|
||||||
day.EventsCollection.CalendarItemRemoved -= CalendarItemRemoved;
|
|
||||||
day.EventsCollection.CalendarItemAdded -= CalendarItemAdded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,7 @@
|
|||||||
"DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.",
|
"DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.",
|
||||||
"Dialog_DontAskAgain": "Don't ask again",
|
"Dialog_DontAskAgain": "Don't ask again",
|
||||||
"CalendarAllDayEventSummary": "all-day events",
|
"CalendarAllDayEventSummary": "all-day events",
|
||||||
|
"CalendarItemAllDay": "all day",
|
||||||
"CreateAccountAliasDialog_Title": "Create Account Alias",
|
"CreateAccountAliasDialog_Title": "Create Account Alias",
|
||||||
"CreateAccountAliasDialog_Description": "Make sure your outgoing server allows sending mails from this alias.",
|
"CreateAccountAliasDialog_Description": "Make sure your outgoing server allows sending mails from this alias.",
|
||||||
"CreateAccountAliasDialog_AliasAddress": "Address",
|
"CreateAccountAliasDialog_AliasAddress": "Address",
|
||||||
|
|||||||
Binary file not shown.
@@ -71,7 +71,25 @@ namespace Wino.Core.UWP.Controls
|
|||||||
{ WinoIconGlyph.Message, "\uE8BD" },
|
{ WinoIconGlyph.Message, "\uE8BD" },
|
||||||
{ WinoIconGlyph.New, "\U000F002A" },
|
{ WinoIconGlyph.New, "\U000F002A" },
|
||||||
{ WinoIconGlyph.Blocked,"\uF140" },
|
{ WinoIconGlyph.Blocked,"\uF140" },
|
||||||
{ WinoIconGlyph.IMAP, "\uE715" }
|
{ WinoIconGlyph.Calendar, "\uE912" },
|
||||||
|
{ WinoIconGlyph.CalendarToday, "\uE911" },
|
||||||
|
{ WinoIconGlyph.CalendarDay, "\uE913" },
|
||||||
|
{ WinoIconGlyph.CalendarWeek, "\uE914" },
|
||||||
|
{ WinoIconGlyph.CalendarMonth, "\uE91c" },
|
||||||
|
{ WinoIconGlyph.CalendarYear, "\uE917" },
|
||||||
|
{ WinoIconGlyph.WeatherBlow, "\uE907" },
|
||||||
|
{ WinoIconGlyph.WeatherCloudy, "\uE920" },
|
||||||
|
{ WinoIconGlyph.WeatherSunny, "\uE90e" },
|
||||||
|
{ WinoIconGlyph.WeatherRainy, "\uE908" },
|
||||||
|
{ WinoIconGlyph.WeatherSnowy, "\uE90a" },
|
||||||
|
{ WinoIconGlyph.WeatherSnowShowerAtNight, "\uE90c" },
|
||||||
|
{ WinoIconGlyph.WeatherThunderstorm, "\uE906" },
|
||||||
|
{ WinoIconGlyph.CalendarEventRepeat, "\uE915" },
|
||||||
|
{ WinoIconGlyph.CalendarEventMuiltiDay, "\uE91b" },
|
||||||
|
{ WinoIconGlyph.Reminder, "\uE918" },
|
||||||
|
{ WinoIconGlyph.CalendarAttendee, "\uE91a" },
|
||||||
|
{ WinoIconGlyph.CalendarSync, "\uE91d" },
|
||||||
|
{ WinoIconGlyph.CalendarError, "\uE916" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,27 @@ namespace Wino.Core.UWP.Controls
|
|||||||
Message,
|
Message,
|
||||||
New,
|
New,
|
||||||
IMAP,
|
IMAP,
|
||||||
Print
|
Print,
|
||||||
|
Calendar,
|
||||||
|
CalendarToday,
|
||||||
|
CalendarDay,
|
||||||
|
CalendarWeek,
|
||||||
|
CalendarWorkWeek,
|
||||||
|
CalendarMonth,
|
||||||
|
CalendarYear,
|
||||||
|
WeatherBlow,
|
||||||
|
WeatherCloudy,
|
||||||
|
WeatherSunny,
|
||||||
|
WeatherRainy,
|
||||||
|
WeatherSnowy,
|
||||||
|
WeatherSnowShowerAtNight,
|
||||||
|
WeatherThunderstorm,
|
||||||
|
CalendarEventRepeat,
|
||||||
|
CalendarEventMuiltiDay,
|
||||||
|
CalendarError,
|
||||||
|
Reminder,
|
||||||
|
CalendarAttendee,
|
||||||
|
CalendarSync,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WinoFontIcon : FontIcon
|
public class WinoFontIcon : FontIcon
|
||||||
|
|||||||
@@ -265,6 +265,12 @@ namespace Wino.Core.UWP.Services
|
|||||||
set => SaveProperty(propertyName: nameof(WorkingDayEnd), value);
|
set => SaveProperty(propertyName: nameof(WorkingDayEnd), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool GhostRenderAllDayEvents
|
||||||
|
{
|
||||||
|
get => _configurationService.Get(nameof(GhostRenderAllDayEvents), true);
|
||||||
|
set => SaveProperty(nameof(GhostRenderAllDayEvents), value);
|
||||||
|
}
|
||||||
|
|
||||||
public CalendarSettings GetCurrentCalendarSettings()
|
public CalendarSettings GetCurrentCalendarSettings()
|
||||||
{
|
{
|
||||||
var workingDays = GetDaysBetween(WorkingDayStart, WorkingDayEnd);
|
var workingDays = GetDaysBetween(WorkingDayStart, WorkingDayEnd);
|
||||||
@@ -275,7 +281,8 @@ namespace Wino.Core.UWP.Services
|
|||||||
WorkingHourEnd,
|
WorkingHourEnd,
|
||||||
HourHeight,
|
HourHeight,
|
||||||
Prefer24HourTimeFormat ? DayHeaderDisplayType.TwentyFourHour : DayHeaderDisplayType.TwelveHour,
|
Prefer24HourTimeFormat ? DayHeaderDisplayType.TwentyFourHour : DayHeaderDisplayType.TwelveHour,
|
||||||
new CultureInfo(WinoTranslationDictionary.GetLanguageFileNameRelativePath(CurrentLanguage)));
|
new CultureInfo(WinoTranslationDictionary.GetLanguageFileNameRelativePath(CurrentLanguage)),
|
||||||
|
GhostRenderAllDayEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<DayOfWeek> GetDaysBetween(DayOfWeek startDay, DayOfWeek endDay)
|
private List<DayOfWeek> GetDaysBetween(DayOfWeek startDay, DayOfWeek endDay)
|
||||||
|
|||||||
@@ -202,19 +202,7 @@ namespace Wino.Core.Extensions
|
|||||||
return calendar;
|
return calendar;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public static DateTimeOffset? GetEventDateTimeOffset(EventDateTime calendarEvent)
|
||||||
/// Extracts the start DateTimeOffset of a Google Calendar Event.
|
|
||||||
/// Handles different date/time representations (date-only, date-time, recurring events).
|
|
||||||
/// Uses the DateTimeDateTimeOffset property for optimal performance and accuracy.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="calendarEvent">The Google Calendar Event object.</param>
|
|
||||||
/// <returns>The start DateTimeOffset of the event, or null if it cannot be determined.</returns>
|
|
||||||
//public static DateTimeOffset GetEventStartDateTimeOffset(this Event calendarEvent)
|
|
||||||
//{
|
|
||||||
// return GetEventDateTimeOffset(calendarEvent.Start);
|
|
||||||
//}
|
|
||||||
|
|
||||||
public static DateTimeOffset GetEventDateTimeOffset(EventDateTime calendarEvent)
|
|
||||||
{
|
{
|
||||||
if (calendarEvent != null)
|
if (calendarEvent != null)
|
||||||
{
|
{
|
||||||
@@ -236,45 +224,9 @@ namespace Wino.Core.Extensions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Exception("Google Calendar event has no date.");
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the duration of a Google Calendar Event in seconds.
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
/// <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>
|
|
||||||
//public static int GetEventDurationInSeconds(this Event calendarEvent)
|
|
||||||
//{
|
|
||||||
// var start = calendarEvent.GetEventStartDateTimeOffset();
|
|
||||||
|
|
||||||
// DateTimeOffset? end = null;
|
|
||||||
// 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
|
|
||||||
// {
|
|
||||||
// throw new Exception("Invalid date format in Google Calendar event end date.");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (end == null) throw new Exception("Google Calendar event has no end date.");
|
|
||||||
|
|
||||||
// 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>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ namespace Wino.Core.Integration.Processors
|
|||||||
{
|
{
|
||||||
Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId);
|
Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId);
|
||||||
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
|
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
|
||||||
Task<CalendarItem> CreateCalendarItemAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount);
|
Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IOutlookChangeProcessor : IDefaultChangeProcessor
|
public interface IOutlookChangeProcessor : IDefaultChangeProcessor
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Google.Apis.Calendar.v3.Data;
|
using Google.Apis.Calendar.v3.Data;
|
||||||
|
using Serilog;
|
||||||
using Wino.Core.Domain.Entities.Calendar;
|
using Wino.Core.Domain.Entities.Calendar;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
@@ -32,125 +33,250 @@ namespace Wino.Core.Integration.Processors
|
|||||||
public Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
|
public Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
|
||||||
=> MailService.CreateAssignmentAsync(accountId, mailCopyId, remoteFolderId);
|
=> MailService.CreateAssignmentAsync(accountId, mailCopyId, remoteFolderId);
|
||||||
|
|
||||||
public async Task<CalendarItem> CreateCalendarItemAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
|
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
|
||||||
{
|
{
|
||||||
var eventStartDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.Start);
|
var status = calendarEvent.Status;
|
||||||
var eventEndDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.End);
|
|
||||||
|
|
||||||
var totalDurationInSeconds = (eventEndDateTimeOffset - eventStartDateTimeOffset).TotalSeconds;
|
var recurringEventId = calendarEvent.RecurringEventId;
|
||||||
|
|
||||||
var calendarItem = new CalendarItem()
|
// 1. Canceled exceptions of recurred events are only guaranteed to have recurringEventId, Id and start time.
|
||||||
|
// 2. Updated exceptions of recurred events have different Id, but recurringEventId is the same as parent.
|
||||||
|
|
||||||
|
// Check if we have this event before.
|
||||||
|
var existingCalendarItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id);
|
||||||
|
|
||||||
|
if (existingCalendarItem == null)
|
||||||
{
|
{
|
||||||
CalendarId = assignedCalendar.Id,
|
CalendarItem parentRecurringEvent = null;
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
|
||||||
Description = calendarEvent.Description,
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
StartDate = eventStartDateTimeOffset.DateTime,
|
|
||||||
StartDateOffset = eventStartDateTimeOffset.Offset,
|
|
||||||
EndDateOffset = eventEndDateTimeOffset.Offset,
|
|
||||||
DurationInSeconds = totalDurationInSeconds,
|
|
||||||
Location = calendarEvent.Location,
|
|
||||||
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
|
|
||||||
Status = GetStatus(calendarEvent.Status),
|
|
||||||
Title = calendarEvent.Summary,
|
|
||||||
UpdatedAt = DateTimeOffset.UtcNow,
|
|
||||||
Visibility = GetVisibility(calendarEvent.Visibility),
|
|
||||||
HtmlLink = calendarEvent.HtmlLink
|
|
||||||
};
|
|
||||||
|
|
||||||
Debug.WriteLine($"({assignedCalendar.Name}) {calendarItem.Title}, Start: {calendarItem.StartDate.ToString("f")}, End: {calendarItem.EndDate.ToString("f")}");
|
// Manage the recurring event id.
|
||||||
|
if (!string.IsNullOrEmpty(recurringEventId))
|
||||||
// TODO: There are some edge cases with cancellation here.
|
|
||||||
CalendarItemStatus GetStatus(string status)
|
|
||||||
{
|
|
||||||
return status switch
|
|
||||||
{
|
{
|
||||||
"confirmed" => CalendarItemStatus.Confirmed,
|
parentRecurringEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, recurringEventId).ConfigureAwait(false);
|
||||||
"tentative" => CalendarItemStatus.Tentative,
|
|
||||||
"cancelled" => CalendarItemStatus.Cancelled,
|
|
||||||
_ => CalendarItemStatus.Confirmed
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarItemVisibility GetVisibility(string visibility)
|
if (parentRecurringEvent == null)
|
||||||
{
|
{
|
||||||
/// Visibility of the event. Optional. Possible values are: - "default" - Uses the default visibility for
|
Log.Information($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}");
|
||||||
/// events on the calendar. This is the default value. - "public" - The event is public and event details are
|
return;
|
||||||
/// visible to all readers of the calendar. - "private" - The event is private and only event attendees may
|
}
|
||||||
/// view event details. - "confidential" - The event is private. This value is provided for compatibility
|
}
|
||||||
/// reasons.
|
|
||||||
|
|
||||||
return visibility switch
|
// We don't have this event yet. Create a new one.
|
||||||
|
var eventStartDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.Start);
|
||||||
|
var eventEndDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.End);
|
||||||
|
|
||||||
|
double totalDurationInSeconds = 0;
|
||||||
|
|
||||||
|
if (eventStartDateTimeOffset != null && eventEndDateTimeOffset != null)
|
||||||
{
|
{
|
||||||
"default" => CalendarItemVisibility.Default,
|
totalDurationInSeconds = (eventEndDateTimeOffset.Value - eventStartDateTimeOffset.Value).TotalSeconds;
|
||||||
"public" => CalendarItemVisibility.Public,
|
}
|
||||||
"private" => CalendarItemVisibility.Private,
|
|
||||||
"confidential" => CalendarItemVisibility.Confidential,
|
|
||||||
_ => CalendarItemVisibility.Default
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attendees
|
CalendarItem calendarItem = null;
|
||||||
var attendees = new List<CalendarEventAttendee>();
|
|
||||||
|
|
||||||
if (calendarEvent.Attendees == null)
|
if (parentRecurringEvent != null)
|
||||||
{
|
|
||||||
// Self-only event.
|
|
||||||
|
|
||||||
attendees.Add(new CalendarEventAttendee()
|
|
||||||
{
|
{
|
||||||
CalendarItemId = calendarItem.Id,
|
// Exceptions of parent events might not have all the fields populated.
|
||||||
IsOrganizer = true,
|
// We must use the parent event's data for fields that don't exists.
|
||||||
Email = organizerAccount.Address,
|
|
||||||
Name = organizerAccount.SenderName,
|
// Update duration if it's not populated.
|
||||||
AttendenceStatus = AttendeeStatus.Accepted,
|
if (totalDurationInSeconds == 0)
|
||||||
Id = Guid.NewGuid(),
|
{
|
||||||
IsOptionalAttendee = false,
|
totalDurationInSeconds = parentRecurringEvent.DurationInSeconds;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
calendarItem = new CalendarItem()
|
||||||
|
{
|
||||||
|
CalendarId = assignedCalendar.Id,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
Description = calendarEvent.Description ?? parentRecurringEvent.Description,
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
StartDate = eventStartDateTimeOffset.Value.DateTime,
|
||||||
|
StartDateOffset = eventStartDateTimeOffset.Value.Offset,
|
||||||
|
EndDateOffset = eventEndDateTimeOffset?.Offset ?? parentRecurringEvent.EndDateOffset,
|
||||||
|
DurationInSeconds = totalDurationInSeconds,
|
||||||
|
Location = string.IsNullOrEmpty(calendarEvent.Location) ? parentRecurringEvent.Location : calendarEvent.Location,
|
||||||
|
|
||||||
|
// Leave it empty if it's not populated.
|
||||||
|
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent) == null ? string.Empty : GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
|
||||||
|
Status = GetStatus(calendarEvent.Status),
|
||||||
|
Title = string.IsNullOrEmpty(calendarEvent.Summary) ? parentRecurringEvent.Title : calendarEvent.Summary,
|
||||||
|
UpdatedAt = DateTimeOffset.UtcNow,
|
||||||
|
Visibility = string.IsNullOrEmpty(calendarEvent.Visibility) ? parentRecurringEvent.Visibility : GetVisibility(calendarEvent.Visibility),
|
||||||
|
HtmlLink = calendarEvent.HtmlLink,
|
||||||
|
RemoteEventId = calendarEvent.Id,
|
||||||
|
IsLocked = calendarEvent.Locked.GetValueOrDefault()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This is a parent event creation.
|
||||||
|
// Start-End dates are guaranteed to be populated.
|
||||||
|
|
||||||
|
if (eventStartDateTimeOffset == null || eventEndDateTimeOffset == null)
|
||||||
|
{
|
||||||
|
Log.Error("Failed to create parent event because either start or end date is not specified.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarItem = new CalendarItem()
|
||||||
|
{
|
||||||
|
CalendarId = assignedCalendar.Id,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
Description = calendarEvent.Description,
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
StartDate = eventStartDateTimeOffset.Value.DateTime,
|
||||||
|
StartDateOffset = eventStartDateTimeOffset.Value.Offset,
|
||||||
|
EndDateOffset = eventEndDateTimeOffset.Value.Offset,
|
||||||
|
DurationInSeconds = totalDurationInSeconds,
|
||||||
|
Location = calendarEvent.Location,
|
||||||
|
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
|
||||||
|
Status = GetStatus(calendarEvent.Status),
|
||||||
|
Title = calendarEvent.Summary,
|
||||||
|
UpdatedAt = DateTimeOffset.UtcNow,
|
||||||
|
Visibility = GetVisibility(calendarEvent.Visibility),
|
||||||
|
HtmlLink = calendarEvent.HtmlLink,
|
||||||
|
RemoteEventId = calendarEvent.Id,
|
||||||
|
IsLocked = calendarEvent.Locked.GetValueOrDefault()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide canceled events.
|
||||||
|
calendarItem.IsHidden = calendarItem.Status == CalendarItemStatus.Cancelled;
|
||||||
|
|
||||||
|
// Manage the recurring event id.
|
||||||
|
if (parentRecurringEvent != null)
|
||||||
|
{
|
||||||
|
calendarItem.RecurringCalendarItemId = parentRecurringEvent.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"({assignedCalendar.Name}) {calendarItem.Title}, Start: {calendarItem.StartDate.ToString("f")}, End: {calendarItem.EndDate.ToString("f")}");
|
||||||
|
|
||||||
|
// Attendees
|
||||||
|
var attendees = new List<CalendarEventAttendee>();
|
||||||
|
|
||||||
|
//if (calendarEvent.Attendees == null)
|
||||||
|
//{
|
||||||
|
// // Self-only event.
|
||||||
|
|
||||||
|
// attendees.Add(new CalendarEventAttendee()
|
||||||
|
// {
|
||||||
|
// CalendarItemId = calendarItem.Id,
|
||||||
|
// IsOrganizer = true,
|
||||||
|
// Email = organizerAccount.Address,
|
||||||
|
// Name = organizerAccount.SenderName,
|
||||||
|
// AttendenceStatus = AttendeeStatus.Accepted,
|
||||||
|
// Id = Guid.NewGuid(),
|
||||||
|
// IsOptionalAttendee = false,
|
||||||
|
// });
|
||||||
|
//}
|
||||||
|
//else
|
||||||
|
//{
|
||||||
|
// foreach (var attendee in calendarEvent.Attendees)
|
||||||
|
// {
|
||||||
|
// if (attendee.Self == true)
|
||||||
|
// {
|
||||||
|
// // TODO:
|
||||||
|
// }
|
||||||
|
// else if (!string.IsNullOrEmpty(attendee.Email))
|
||||||
|
// {
|
||||||
|
// AttendeeStatus GetAttendenceStatus(string responseStatus)
|
||||||
|
// {
|
||||||
|
// return responseStatus switch
|
||||||
|
// {
|
||||||
|
// "accepted" => AttendeeStatus.Accepted,
|
||||||
|
// "declined" => AttendeeStatus.Declined,
|
||||||
|
// "tentative" => AttendeeStatus.Tentative,
|
||||||
|
// "needsAction" => AttendeeStatus.NeedsAction,
|
||||||
|
// _ => AttendeeStatus.NeedsAction
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var eventAttendee = new CalendarEventAttendee()
|
||||||
|
// {
|
||||||
|
// CalendarItemId = calendarItem.Id,
|
||||||
|
// IsOrganizer = attendee.Organizer ?? false,
|
||||||
|
// Comment = attendee.Comment,
|
||||||
|
// Email = attendee.Email,
|
||||||
|
// Name = attendee.DisplayName,
|
||||||
|
// AttendenceStatus = GetAttendenceStatus(attendee.ResponseStatus),
|
||||||
|
// Id = Guid.NewGuid(),
|
||||||
|
// IsOptionalAttendee = attendee.Optional ?? false,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// attendees.Add(eventAttendee);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
await CalendarService.CreateNewCalendarItemAsync(calendarItem, attendees);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var attendee in calendarEvent.Attendees)
|
// We have this event already. Update it.
|
||||||
|
if (calendarEvent.Status == "cancelled")
|
||||||
{
|
{
|
||||||
if (attendee.Self == true)
|
// Event is canceled.
|
||||||
{
|
|
||||||
// TODO:
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(attendee.Email))
|
|
||||||
{
|
|
||||||
AttendeeStatus GetAttendenceStatus(string responseStatus)
|
|
||||||
{
|
|
||||||
return responseStatus switch
|
|
||||||
{
|
|
||||||
"accepted" => AttendeeStatus.Accepted,
|
|
||||||
"declined" => AttendeeStatus.Declined,
|
|
||||||
"tentative" => AttendeeStatus.Tentative,
|
|
||||||
"needsAction" => AttendeeStatus.NeedsAction,
|
|
||||||
_ => AttendeeStatus.NeedsAction
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var eventAttendee = new CalendarEventAttendee()
|
// Parent event is canceled. We must delete everything.
|
||||||
{
|
if (string.IsNullOrEmpty(recurringEventId))
|
||||||
CalendarItemId = calendarItem.Id,
|
{
|
||||||
IsOrganizer = attendee.Organizer ?? false,
|
Log.Information("Parent event is canceled. Deleting all instances of {Id}", existingCalendarItem.Id);
|
||||||
Comment = attendee.Comment,
|
|
||||||
Email = attendee.Email,
|
|
||||||
Name = attendee.DisplayName,
|
|
||||||
AttendenceStatus = GetAttendenceStatus(attendee.ResponseStatus),
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
IsOptionalAttendee = attendee.Optional ?? false,
|
|
||||||
};
|
|
||||||
|
|
||||||
attendees.Add(eventAttendee);
|
await CalendarService.DeleteCalendarItemAsync(existingCalendarItem.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Child event is canceled.
|
||||||
|
// Child should live as long as parent lives, but must not be displayed to the user.
|
||||||
|
|
||||||
|
existingCalendarItem.IsHidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Make sure to unhide the event.
|
||||||
|
// It might be marked as hidden before.
|
||||||
|
existingCalendarItem.IsHidden = false;
|
||||||
|
|
||||||
|
// Update the event properties.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upsert the event.
|
||||||
await CalendarService.CreateNewCalendarItemAsync(calendarItem, attendees);
|
await Connection.InsertOrReplaceAsync(existingCalendarItem);
|
||||||
|
|
||||||
return calendarItem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CalendarItemStatus GetStatus(string status)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
"confirmed" => CalendarItemStatus.Confirmed,
|
||||||
|
"tentative" => CalendarItemStatus.Tentative,
|
||||||
|
"cancelled" => CalendarItemStatus.Cancelled,
|
||||||
|
_ => CalendarItemStatus.Confirmed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private CalendarItemVisibility GetVisibility(string visibility)
|
||||||
|
{
|
||||||
|
/// Visibility of the event. Optional. Possible values are: - "default" - Uses the default visibility for
|
||||||
|
/// events on the calendar. This is the default value. - "public" - The event is public and event details are
|
||||||
|
/// visible to all readers of the calendar. - "private" - The event is private and only event attendees may
|
||||||
|
/// view event details. - "confidential" - The event is private. This value is provided for compatibility
|
||||||
|
/// reasons.
|
||||||
|
|
||||||
|
return visibility switch
|
||||||
|
{
|
||||||
|
"default" => CalendarItemVisibility.Default,
|
||||||
|
"public" => CalendarItemVisibility.Public,
|
||||||
|
"private" => CalendarItemVisibility.Private,
|
||||||
|
"confidential" => CalendarItemVisibility.Confidential,
|
||||||
|
_ => CalendarItemVisibility.Default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -350,13 +350,18 @@ namespace Wino.Core.Synchronizers.Mail
|
|||||||
|
|
||||||
calendar.SynchronizationDeltaToken = syncToken;
|
calendar.SynchronizationDeltaToken = syncToken;
|
||||||
|
|
||||||
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
|
// allEvents contains new or updated events.
|
||||||
|
// Process them and create/update local calendar items.
|
||||||
|
|
||||||
foreach (var @event in allEvents)
|
foreach (var @event in allEvents)
|
||||||
{
|
{
|
||||||
// TODO: Exception handling for event processing.
|
// TODO: Exception handling for event processing.
|
||||||
await _gmailChangeProcessor.CreateCalendarItemAsync(@event, calendar, Account).ConfigureAwait(false);
|
// TODO: Also update attendees and other properties.
|
||||||
|
|
||||||
|
await _gmailChangeProcessor.ManageCalendarEventAsync(@event, calendar, Account).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return default;
|
return default;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
@@ -59,9 +60,23 @@ namespace Wino.Services
|
|||||||
|
|
||||||
if (calendarItem == null) return;
|
if (calendarItem == null) return;
|
||||||
|
|
||||||
await Connection.Table<CalendarItem>().DeleteAsync(x => x.Id == calendarItemId);
|
List<CalendarItem> eventsToRemove = new() { calendarItem };
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(calendarItem));
|
if (!string.IsNullOrEmpty(calendarItem.Recurrence))
|
||||||
|
{
|
||||||
|
// Delete recurring events as well.
|
||||||
|
var recurringEvents = await Connection.Table<CalendarItem>().Where(a => a.RecurringCalendarItemId == calendarItemId).ToListAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
eventsToRemove.AddRange(recurringEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var @event in eventsToRemove)
|
||||||
|
{
|
||||||
|
await Connection.Table<CalendarItem>().DeleteAsync(x => x.Id == @event.Id).ConfigureAwait(false);
|
||||||
|
await Connection.Table<CalendarEventAttendee>().DeleteAsync(a => a.CalendarItemId == @event.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
|
WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(@event));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees)
|
public async Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees)
|
||||||
@@ -84,7 +99,9 @@ namespace Wino.Services
|
|||||||
// 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.
|
||||||
|
|
||||||
var accountEvents = await Connection.Table<CalendarItem>().Where(x => x.CalendarId == calendar.Id).ToListAsync();
|
var accountEvents = await Connection.Table<CalendarItem>()
|
||||||
|
.Where(x => x.CalendarId == calendar.Id && !x.IsHidden).ToListAsync();
|
||||||
|
|
||||||
var result = new List<CalendarItem>();
|
var result = new List<CalendarItem>();
|
||||||
|
|
||||||
foreach (var ev in accountEvents)
|
foreach (var ev in accountEvents)
|
||||||
@@ -104,15 +121,17 @@ namespace Wino.Services
|
|||||||
|
|
||||||
if (ev.Period.OverlapsWith(dayRangeRenderModel.Period))
|
if (ev.Period.OverlapsWith(dayRangeRenderModel.Period))
|
||||||
{
|
{
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// This event has recurrences.
|
||||||
|
// Wino stores recurrent events as a separae calendar item, without the recurrence rule.
|
||||||
|
// Because each isntance of recurrent event can have different attendees, properties etc.
|
||||||
|
// Even though the event is recurrent, each updated instance is a separate calendar item.
|
||||||
|
// Calculate the all recurrences, and remove the exceptional instances like hidden ones.
|
||||||
|
|
||||||
var recurrenceLines = Regex.Split(ev.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
|
var recurrenceLines = Regex.Split(ev.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
|
||||||
|
|
||||||
foreach (var line in recurrenceLines)
|
foreach (var line in recurrenceLines)
|
||||||
@@ -123,9 +142,34 @@ namespace Wino.Services
|
|||||||
// Calculate occurrences in the range.
|
// Calculate occurrences in the range.
|
||||||
var occurrences = calendarEvent.GetOccurrences(dayRangeRenderModel.Period.Start, dayRangeRenderModel.Period.End);
|
var occurrences = calendarEvent.GetOccurrences(dayRangeRenderModel.Period.Start, dayRangeRenderModel.Period.End);
|
||||||
|
|
||||||
|
// Get all recurrent exceptional calendar events.
|
||||||
|
var exceptionalRecurrences = await Connection.Table<CalendarItem>()
|
||||||
|
.Where(a => a.RecurringCalendarItemId == ev.Id)
|
||||||
|
.ToListAsync()
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
foreach (var occurrence in occurrences)
|
foreach (var occurrence in occurrences)
|
||||||
{
|
{
|
||||||
result.Add(ev);
|
var singleInstance = exceptionalRecurrences.FirstOrDefault(a =>
|
||||||
|
a.StartDate == occurrence.Period.StartTime.Value &&
|
||||||
|
a.EndDate == occurrence.Period.EndTime.Value);
|
||||||
|
|
||||||
|
if (singleInstance == null)
|
||||||
|
{
|
||||||
|
// This occurrence is not an exceptional instance.
|
||||||
|
// Change the start and end date of the event and add as calendar item.
|
||||||
|
// Other properties are guaranteed to be the same as the parent event.
|
||||||
|
ev.StartDate = occurrence.Period.StartTime.Value;
|
||||||
|
ev.DurationInSeconds = (occurrence.Period.EndTime.Value - occurrence.Period.StartTime.Value).TotalSeconds;
|
||||||
|
|
||||||
|
result.Add(ev);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// There is a single instance of this recurrent event.
|
||||||
|
// It will be added as single item if it's not hidden.
|
||||||
|
// We don't need to do anything here.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,5 +179,25 @@ namespace Wino.Services
|
|||||||
|
|
||||||
public Task<AccountCalendar> GetAccountCalendarAsync(Guid accountCalendarId)
|
public Task<AccountCalendar> GetAccountCalendarAsync(Guid accountCalendarId)
|
||||||
=> Connection.GetAsync<AccountCalendar>(accountCalendarId);
|
=> Connection.GetAsync<AccountCalendar>(accountCalendarId);
|
||||||
|
|
||||||
|
public async Task<CalendarItem> GetCalendarItemAsync(Guid accountCalendarId, string remoteEventId)
|
||||||
|
{
|
||||||
|
var query = new Query()
|
||||||
|
.From(nameof(CalendarItem))
|
||||||
|
.Where(nameof(CalendarItem.CalendarId), accountCalendarId)
|
||||||
|
.Where(nameof(CalendarItem.RemoteEventId), remoteEventId);
|
||||||
|
|
||||||
|
var rawQuery = query.GetRawQuery();
|
||||||
|
|
||||||
|
var calendarItem = await Connection.FindWithQueryAsync<CalendarItem>(rawQuery);
|
||||||
|
|
||||||
|
// Load assigned calendar.
|
||||||
|
if (calendarItem != null)
|
||||||
|
{
|
||||||
|
calendarItem.AssignedCalendar = await Connection.GetAsync<AccountCalendar>(calendarItem.CalendarId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return calendarItem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user