Outlook calendar/event syncing basics without delta. Bunch of UI updates for the calendar view.

This commit is contained in:
Burak Kaan Köse
2025-01-06 02:15:21 +01:00
parent a7674d436d
commit 125c277c88
46 changed files with 1104 additions and 356 deletions

View File

@@ -224,7 +224,7 @@ namespace Wino.Calendar.ViewModels
{
var t = new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions()
{
AccountId = Guid.Parse("52fae547-0740-4aa3-8d51-519bd31278ca"),
AccountId = Guid.Parse("bd0fc1ab-168a-436d-86ce-0661c0eabaf9"),
Type = CalendarSynchronizationType.CalendarMetadata
}, SynchronizationSource.Client);

View File

@@ -79,8 +79,8 @@ namespace Wino.Calendar.ViewModels
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
private string _eventName;
public DateTime QuickEventStartTime => SelectedQuickEventDate.Value.Date.Add(_currentSettings.GetTimeSpan(SelectedStartTimeString).Value);
public DateTime QuickEventEndTime => SelectedQuickEventDate.Value.Date.Add(_currentSettings.GetTimeSpan(SelectedEndTimeString).Value);
public DateTime QuickEventStartTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedStartTimeString).Value);
public DateTime QuickEventEndTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedEndTimeString).Value);
public bool CanSaveQuickEvent => SelectedQuickEventAccountCalendar != null &&
!string.IsNullOrWhiteSpace(EventName) &&
@@ -90,6 +90,8 @@ namespace Wino.Calendar.ViewModels
#endregion
#region Data Initialization
[ObservableProperty]
private DayRangeCollection _dayRanges = [];
@@ -102,6 +104,20 @@ namespace Wino.Calendar.ViewModels
[ObservableProperty]
private bool _isCalendarEnabled = true;
#endregion
#region Event Details
public event EventHandler DetailsShowCalendarItemChanged;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsEventDetailsVisible))]
private CalendarItemViewModel _displayDetailsCalendarItemViewModel;
public bool IsEventDetailsVisible => DisplayDetailsCalendarItemViewModel != null;
#endregion
// TODO: Get rid of some of the items if we have too many.
private const int maxDayRangeSize = 10;
@@ -115,7 +131,9 @@ namespace Wino.Calendar.ViewModels
private SemaphoreSlim _calendarLoadingSemaphore = new(1);
private bool isLoadMoreBlocked = false;
private CalendarSettings _currentSettings = null;
[ObservableProperty]
private CalendarSettings _currentSettings;
public IStatePersistanceService StatePersistanceService { get; }
public IAccountCalendarStateService AccountCalendarStateService { get; }
@@ -148,6 +166,8 @@ namespace Wino.Calendar.ViewModels
var days = dayRangeRenderModels.SelectMany(a => a.CalendarDays);
days.ForEach(a => a.EventsCollection.FilterByCalendars(AccountCalendarStateService.ActiveCalendars.Select(a => a.Id)));
DisplayDetailsCalendarItemViewModel = null;
}
// TODO: Replace when calendar settings are updated.
@@ -156,8 +176,8 @@ namespace Wino.Calendar.ViewModels
{
return displayType switch
{
CalendarDisplayType.Day => new DayCalendarDrawingStrategy(_currentSettings),
CalendarDisplayType.Week => new WeekCalendarDrawingStrategy(_currentSettings),
CalendarDisplayType.Day => new DayCalendarDrawingStrategy(CurrentSettings),
CalendarDisplayType.Week => new WeekCalendarDrawingStrategy(CurrentSettings),
_ => throw new NotImplementedException(),
};
}
@@ -178,6 +198,26 @@ namespace Wino.Calendar.ViewModels
SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary);
}
[RelayCommand]
private void NavigateSeries()
{
}
[RelayCommand]
private void NavigateEventDetails()
{
if (DisplayDetailsCalendarItemViewModel == null) return;
NavigateEvent(DisplayDetailsCalendarItemViewModel);
}
[RelayCommand]
private void NavigateEvent(CalendarItemViewModel calendarItemViewModel)
{
// Double tap or clicked 'view details' of the event detail popup.
}
[RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(CanSaveQuickEvent))]
private async Task SaveQuickEventAsync()
{
@@ -211,13 +251,33 @@ namespace Wino.Calendar.ViewModels
{
IsAllDay = false;
SelectedStartTimeString = _currentSettings.GetTimeString(startTime);
SelectedEndTimeString = _currentSettings.GetTimeString(endTime);
SelectedStartTimeString = CurrentSettings.GetTimeString(startTime);
SelectedEndTimeString = CurrentSettings.GetTimeString(endTime);
}
// Manage event detail popup context and select-unselect the proper items.
// Item selection rules are defined in the selection method.
partial void OnDisplayDetailsCalendarItemViewModelChanging(CalendarItemViewModel oldValue, CalendarItemViewModel newValue)
{
if (oldValue != null)
{
UnselectCalendarItem(oldValue);
}
if (newValue != null)
{
SelectCalendarItem(newValue);
}
}
// Notify view that the detail context changed.
// This will align the event detail popup to the selected event.
partial void OnDisplayDetailsCalendarItemViewModelChanged(CalendarItemViewModel value)
=> DetailsShowCalendarItemChanged?.Invoke(this, EventArgs.Empty);
private void RefreshSettings()
{
_currentSettings = _preferencesService.GetCurrentCalendarSettings();
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
// Populate the hour selection strings.
var timeStrings = new List<string>();
@@ -228,7 +288,7 @@ namespace Wino.Calendar.ViewModels
{
var time = new DateTime(1, 1, 1, hour, minute, 0);
if (_currentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour)
if (CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour)
{
timeStrings.Add(time.ToString("HH:mm"));
}
@@ -255,7 +315,6 @@ namespace Wino.Calendar.ViewModels
// 2. Day display count is different.
// 3. Display date is not in the visible range.
if (DayRanges.DisplayRange == null) return false;
return
@@ -289,6 +348,9 @@ namespace Wino.Calendar.ViewModels
await RenderDatesAsync(message.CalendarInitInitiative,
message.DisplayDate,
CalendarLoadDirection.Replace);
// Scroll to the current hour.
Messenger.Send(new ScrollToHourMessage(TimeSpan.FromHours(DateTime.Now.Hour)));
}
catch (Exception ex)
{
@@ -403,7 +465,7 @@ namespace Wino.Calendar.ViewModels
var endDate = startDate.AddDays(eachFlipItemCount);
var range = new DateRange(startDate, endDate);
var renderOptions = new CalendarRenderOptions(range, _currentSettings);
var renderOptions = new CalendarRenderOptions(range, CurrentSettings);
var dayRangeHeaderModel = new DayRangeRenderModel(renderOptions);
renderModels.Add(dayRangeHeaderModel);
@@ -613,14 +675,9 @@ namespace Wino.Calendar.ViewModels
}
}
partial void OnSelectedQuickEventDateChanged(DateTime? value)
{
}
partial void OnSelectedStartTimeStringChanged(string newValue)
{
var parsedTime = _currentSettings.GetTimeSpan(newValue);
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
if (parsedTime == null)
{
@@ -634,7 +691,7 @@ namespace Wino.Calendar.ViewModels
partial void OnSelectedEndTimeStringChanged(string newValue)
{
var parsedTime = _currentSettings.GetTimeSpan(newValue);
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
if (parsedTime == null)
{
@@ -648,6 +705,8 @@ namespace Wino.Calendar.ViewModels
partial void OnSelectedDayRangeChanged(DayRangeRenderModel value)
{
DisplayDetailsCalendarItemViewModel = null;
if (DayRanges.Count == 0 || SelectedDateRangeIndex < 0) return;
var selectedRange = DayRanges[SelectedDateRangeIndex];
@@ -699,71 +758,63 @@ namespace Wino.Calendar.ViewModels
// Messenger.Send(new LoadCalendarMessage(DateTime.UtcNow.Date, CalendarInitInitiative.App, true));
}
private IEnumerable<CalendarItemViewModel> GetCalendarItems(Guid calendarItemId)
private IEnumerable<CalendarItemViewModel> GetCalendarItems(CalendarItemViewModel calendarItemViewModel, CalendarDayModel selectedDay)
{
// Multi-day events are sprated in multiple days.
// All-day and multi-day events are selected collectively.
// Recurring events must be selected as a single instance.
// We need to find the day that the event is in, and then select the event.
return DayRanges
.SelectMany(a => a.CalendarDays)
.Select(b => b.EventsCollection.GetCalendarItem(calendarItemId))
.Where(c => c != null)
.Cast<CalendarItemViewModel>()
.Distinct();
}
private void ResetSelectedItems()
{
foreach (var item in AccountCalendarStateService.SelectedItems)
if (calendarItemViewModel.IsSingleExceptionalInstance)
{
var items = GetCalendarItems(item.Id);
foreach (var childItem in items)
{
childItem.IsSelected = false;
}
return [calendarItemViewModel];
}
else
{
return DayRanges
.SelectMany(a => a.CalendarDays)
.Select(b => b.EventsCollection.GetCalendarItem(calendarItemViewModel.Id))
.Where(c => c != null)
.Cast<CalendarItemViewModel>()
.Distinct();
}
AccountCalendarStateService.SelectedItems.Clear();
}
public async void Receive(CalendarItemTappedMessage message)
private void UnselectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
{
if (calendarItemViewModel == null) return;
var itemsToUnselect = GetCalendarItems(calendarItemViewModel, calendarDay);
foreach (var item in itemsToUnselect)
{
item.IsSelected = false;
}
}
private void SelectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
{
if (calendarItemViewModel == null) return;
var itemsToSelect = GetCalendarItems(calendarItemViewModel, calendarDay);
foreach (var item in itemsToSelect)
{
item.IsSelected = true;
}
}
public void Receive(CalendarItemTappedMessage message)
{
if (message.CalendarItemViewModel == null) return;
await ExecuteUIThread(() =>
{
var calendarItems = GetCalendarItems(message.CalendarItemViewModel.Id);
if (!_keyPressService.IsCtrlKeyPressed())
{
ResetSelectedItems();
}
foreach (var item in calendarItems)
{
item.IsSelected = !item.IsSelected;
// Multi-select logic.
if (item.IsSelected && !AccountCalendarStateService.SelectedItems.Contains(item))
{
AccountCalendarStateService.SelectedItems.Add(message.CalendarItemViewModel);
}
else if (!item.IsSelected && AccountCalendarStateService.SelectedItems.Contains(item))
{
AccountCalendarStateService.SelectedItems.Remove(item);
}
}
});
DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel;
}
public void Receive(CalendarItemDoubleTappedMessage message)
{
// TODO: Navigate to the event details page.
}
public void Receive(CalendarItemDoubleTappedMessage message) => NavigateEvent(message.CalendarItemViewModel);
public void Receive(CalendarItemRightTappedMessage message)
{
}
public async void Receive(CalendarItemDeleted message)
@@ -777,9 +828,7 @@ namespace Wino.Calendar.ViewModels
// Event might be spreaded into multiple days.
// Remove from all.
var calendarItems = GetCalendarItems(deletedItem.Id);
// var calendarItems = GetCalendarItems(deletedItem.Id);
});
}
}

View File

@@ -21,9 +21,6 @@ namespace Wino.Calendar.ViewModels
[ObservableProperty]
private bool _is24HourHeaders;
[ObservableProperty]
private bool _ghostRenderAllDayEvents;
[ObservableProperty]
private TimeSpan _workingHourStart;
@@ -64,7 +61,6 @@ namespace Wino.Calendar.ViewModels
_workingHourStart = preferencesService.WorkingHourStart;
_workingHourEnd = preferencesService.WorkingHourEnd;
_cellHourHeight = preferencesService.HourHeight;
_ghostRenderAllDayEvents = preferencesService.GhostRenderAllDayEvents;
_workingDayStartIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
_workingDayEndIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
@@ -79,7 +75,6 @@ namespace Wino.Calendar.ViewModels
partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings();
partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings();
partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
partial void OnGhostRenderAllDayEventsChanged(bool value) => SaveSettings();
public void SaveSettings()
{

View File

@@ -24,11 +24,13 @@ namespace Wino.Calendar.ViewModels.Data
public ITimePeriod Period => CalendarItem.Period;
public bool IsAllDayEvent => ((ICalendarItem)CalendarItem).IsAllDayEvent;
public bool IsAllDayEvent => CalendarItem.IsAllDayEvent;
public bool IsMultiDayEvent => ((ICalendarItem)CalendarItem).IsMultiDayEvent;
public bool IsMultiDayEvent => CalendarItem.IsMultiDayEvent;
public bool IsRecurringEvent => !string.IsNullOrEmpty(CalendarItem.Recurrence);
public bool IsRecurringEvent => CalendarItem.IsRecurringEvent;
public bool IsSingleExceptionalInstance => CalendarItem.IsSingleExceptionalInstance;
[ObservableProperty]
private bool _isSelected;

View File

@@ -22,9 +22,6 @@ namespace Wino.Calendar.ViewModels.Interfaces
public void AddAccountCalendar(AccountCalendarViewModel accountCalendar);
public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar);
ObservableCollection<CalendarItemViewModel> SelectedItems { get; }
bool HasMultipleSelectedItems { get; }
/// <summary>
/// Enumeration of currently selected calendars.
/// </summary>

View File

@@ -1,14 +1,17 @@
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.ViewModels.Messages
{
public class CalendarItemTappedMessage
{
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel)
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel, CalendarDayModel clickedPeriod)
{
CalendarItemViewModel = calendarItemViewModel;
ClickedPeriod = clickedPeriod;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
public CalendarDayModel ClickedPeriod { get; }
}
}

View File

@@ -1,93 +0,0 @@
<UserControl
x:Class="Wino.Calendar.Controls.AllDayItemsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:calendarHelpers="using:Wino.Calendar.Helpers"
xmlns:controls="using:Wino.Calendar.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:data="using:Wino.Calendar.ViewModels.Data"
xmlns:domain="using:Wino.Core.Domain"
xmlns:helpers="using:Wino.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:selectors="using:Wino.Calendar.Selectors"
x:Name="AllDayControl"
d:DesignHeight="300"
d:DesignWidth="400"
mc:Ignorable="d">
<Grid>
<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"
CalendarItem="{x:Bind calendarHelpers:CalendarXamlHelpers.GetFirstAllDayEvent(EventCollection), Mode=OneWay}"
Visibility="{x:Bind helpers:XamlHelpers.CountToVisibilityConverter(EventCollection.AllDayEvents.Count), Mode=OneWay}" />
<Button
x:Name="AllDayItemsSummaryButton"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Visibility="Collapsed">
<Button.Flyout>
<Flyout Placement="Bottom">
<ScrollViewer>
<ItemsControl ItemTemplate="{x:Bind RegularEventItemTemplate}" ItemsSource="{x:Bind EventCollection.AllDayEvents}" />
</ScrollViewer>
</Flyout>
</Button.Flyout>
</Button>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ViewStates">
<VisualState x:Name="FullView" />
<VisualState x:Name="SummaryView">
<VisualState.Setters>
<Setter Target="SingleAllDayEventHolder.Visibility" Value="Collapsed" />
<Setter Target="AllDayItemsSummaryButton.Visibility" Value="Visible" />
<Setter Target="AllDayItemsSummaryButton.Content">
<Setter.Value>
<TextBlock>
<Run Text="{x:Bind EventCollection.AllDayEvents.Count, Mode=OneWay, TargetNullValue='0'}" /> <Run Text="{x:Bind domain:Translator.CalendarAllDayEventSummary}" />
</TextBlock>
</Setter.Value>
</Setter>
</VisualState.Setters>
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind helpers:XamlHelpers.IsMultiple(EventCollection.AllDayEvents.Count), Mode=OneWay, FallbackValue='False'}" />
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>-->
</Grid>
</UserControl>

View File

@@ -1,27 +0,0 @@
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls
{
public sealed partial class AllDayItemsControl : UserControl
{
#region Dependency Properties
public static readonly DependencyProperty CalendarDayModelProperty = DependencyProperty.Register(nameof(CalendarDayModel), typeof(CalendarDayModel), typeof(AllDayItemsControl), new PropertyMetadata(null));
public CalendarDayModel CalendarDayModel
{
get { return (CalendarDayModel)GetValue(CalendarDayModelProperty); }
set { SetValue(CalendarDayModelProperty, value); }
}
#endregion
public AllDayItemsControl()
{
InitializeComponent();
}
}
}

View File

@@ -42,6 +42,7 @@
<Rectangle
x:Name="MainBorder"
Grid.ColumnSpan="2"
Canvas.ZIndex="2"
Stroke="{ThemeResource CalendarItemBorderBrush}"
StrokeThickness="0" />
@@ -61,18 +62,19 @@
<StackPanel
x:Name="AttributeStack"
Grid.Column="1"
Margin="0,4,4,0"
Margin="0,4,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Orientation="Horizontal">
Orientation="Horizontal"
Spacing="6">
<controls:WinoFontIcon
FontSize="10"
FontSize="12"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
Icon="CalendarEventRepeat"
Visibility="{x:Bind CalendarItem.IsRecurringEvent, Mode=OneWay}" />
<controls:WinoFontIcon
FontSize="16"
FontSize="12"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
Icon="CalendarEventMuiltiDay"
Visibility="{x:Bind CalendarItem.IsMultiDayEvent, Mode=OneWay}" />
@@ -83,7 +85,8 @@
<VisualState x:Name="NonSelected" />
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Target="MainBorder.StrokeThickness" Value="2" />
<Setter Target="MainBorder.StrokeThickness" Value="1" />
<Setter Target="MainBorder.Margin" Value="1" />
<Setter Target="MainBorder.Stroke" Value="{ThemeResource CalendarItemSelectedBorderBrush}" />
</VisualState.Setters>
<VisualState.StateTriggers>
@@ -105,20 +108,27 @@
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="EventDurationStates">
<!-- Regular event template in the panel. -->
<VisualState x:Name="RegularEvent" />
<!-- All-Day template for top area. -->
<VisualState x:Name="AllDayEvent">
<VisualState.Setters>
<Setter Target="AttributeStack.VerticalAlignment" Value="Center" />
<Setter Target="MainGrid.MinHeight" Value="30" />
<Setter Target="MainBorder.StrokeThickness" Value="0.5" />
<Setter Target="MainBorder.StrokeThickness" Value="0" />
</VisualState.Setters>
</VisualState>
<!-- Multi-Day template for top area. -->
<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="MainBorder.StrokeThickness" Value="0" />
<Setter Target="AttributeStack.Visibility" Value="Visible" />
<Setter Target="AttributeStack.Margin" Value="0,0,4,0" />
<Setter Target="AttributeStack.VerticalAlignment" Value="Center" />
<Setter Target="MainGrid.MinHeight" Value="30" />
<Setter Target="EventTitleTextblock.HorizontalAlignment" Value="Stretch" />
<Setter Target="EventTitleTextblock.HorizontalTextAlignment" Value="Left" />
@@ -126,11 +136,18 @@
</VisualState.Setters>
</VisualState>
<!--
Ghost rendering for multi-day events in the panel.
All-Multi day area template is CustomAreaMultiDayEvent
-->
<VisualState x:Name="MultiDayEvent">
<VisualState.Setters>
<Setter Target="MainGrid.CornerRadius" Value="0" />
<Setter Target="MainBackground.Opacity" Value="0.2" />
<Setter Target="MainGrid.IsHitTestVisible" Value="False" />
<Setter Target="MainBorder.StrokeThickness" Value="0.5" />
<Setter Target="MainBorder.StrokeThickness" Value="0" />
<Setter Target="AttributeStack.Visibility" Value="Collapsed" />
<Setter Target="EventTitleTextblock.Visibility" Value="Collapsed" />
</VisualState.Setters>

View File

@@ -1,8 +1,10 @@
using System.Diagnostics;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Itenso.TimePeriod;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Messages;
using Wino.Core.Domain;
@@ -12,7 +14,8 @@ namespace Wino.Calendar.Controls
{
public sealed partial class CalendarItemControl : UserControl
{
public bool IsAllDayMultiDayEvent { get; set; }
// Single tap has a delay to report double taps properly.
private bool isSingleTap = false;
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));
@@ -163,21 +166,30 @@ namespace Wino.Calendar.Controls
private void ControlDropped(UIElement sender, DropCompletedEventArgs args) => IsDragging = false;
private void ControlTapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
private async void ControlTapped(object sender, TappedRoutedEventArgs e)
{
if (CalendarItem == null) return;
WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem));
isSingleTap = true;
await Task.Delay(100);
if (isSingleTap)
{
WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem, DisplayingDate));
}
}
private void ControlDoubleTapped(object sender, Windows.UI.Xaml.Input.DoubleTappedRoutedEventArgs e)
private void ControlDoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
if (CalendarItem == null) return;
isSingleTap = false;
WeakReferenceMessenger.Default.Send(new CalendarItemDoubleTappedMessage(CalendarItem));
}
private void ControlRightTapped(object sender, Windows.UI.Xaml.Input.RightTappedRoutedEventArgs e)
private void ControlRightTapped(object sender, RightTappedRoutedEventArgs e)
{
if (CalendarItem == null) return;
@@ -188,10 +200,6 @@ namespace Wino.Calendar.Controls
{
if (CalendarItem == null) return;
if (!CalendarItem.IsSelected)
{
WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem));
}
}
}
}

View File

@@ -24,6 +24,8 @@ namespace Wino.Calendar.Controls
// Hide navigation buttons
PreviousButton.Opacity = NextButton.Opacity = 0;
PreviousButton.IsHitTestVisible = NextButton.IsHitTestVisible = false;
var t = FindName("ScrollingHost");
}
public void GoPreviousFlip()
@@ -37,6 +39,5 @@ namespace Wino.Calendar.Controls
var nextPeer = new ButtonAutomationPeer(NextButton);
nextPeer.Invoke();
}
}
}

View File

@@ -19,7 +19,7 @@ namespace Wino.Calendar.Controls
private TextBlock HeaderDateDayText;
private TextBlock ColumnHeaderText;
private Border IsTodayBorder;
private AllDayItemsControl AllDayItemsControl;
private ItemsControl AllDayItemsControl;
public CalendarDayModel DayModel
{
@@ -41,7 +41,7 @@ namespace Wino.Calendar.Controls
HeaderDateDayText = GetTemplateChild(PART_HeaderDateDayText) as TextBlock;
ColumnHeaderText = GetTemplateChild(PART_ColumnHeaderText) as TextBlock;
IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border;
AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as AllDayItemsControl;
AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as ItemsControl;
UpdateValues();
}
@@ -61,7 +61,7 @@ namespace Wino.Calendar.Controls
HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString();
ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo);
AllDayItemsControl.CalendarDayModel = DayModel;
AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents;
bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date;

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Calendar.Args;
@@ -18,6 +19,8 @@ namespace Wino.Calendar.Controls
public event EventHandler<TimelineCellSelectedArgs> TimelineCellSelected;
public event EventHandler<TimelineCellUnselectedArgs> TimelineCellUnselected;
public event EventHandler ScrollPositionChanging;
#region Dependency Properties
public static readonly DependencyProperty DayRangesProperty = DependencyProperty.Register(nameof(DayRanges), typeof(ObservableCollection<DayRangeRenderModel>), typeof(WinoCalendarControl), new PropertyMetadata(null));
@@ -25,6 +28,7 @@ namespace Wino.Calendar.Controls
public static readonly DependencyProperty SelectedFlipViewDayRangeProperty = DependencyProperty.Register(nameof(SelectedFlipViewDayRange), typeof(DayRangeRenderModel), typeof(WinoCalendarControl), new PropertyMetadata(null));
public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnActiveCanvasChanged)));
public static readonly DependencyProperty IsFlipIdleProperty = DependencyProperty.Register(nameof(IsFlipIdle), typeof(bool), typeof(WinoCalendarControl), new PropertyMetadata(true, new PropertyChangedCallback(OnIdleStateChanged)));
public static readonly DependencyProperty ActiveScrollViewerProperty = DependencyProperty.Register(nameof(ActiveScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnActiveVerticalScrollViewerChanged)));
public DayRangeRenderModel SelectedFlipViewDayRange
{
@@ -32,6 +36,12 @@ namespace Wino.Calendar.Controls
set { SetValue(SelectedFlipViewDayRangeProperty, value); }
}
public ScrollViewer ActiveScrollViewer
{
get { return (ScrollViewer)GetValue(ActiveScrollViewerProperty); }
set { SetValue(ActiveScrollViewerProperty, value); }
}
public WinoDayTimelineCanvas ActiveCanvas
{
get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); }
@@ -79,6 +89,22 @@ namespace Wino.Calendar.Controls
}
}
private static void OnActiveVerticalScrollViewerChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
{
if (calendar is WinoCalendarControl calendarControl)
{
if (e.OldValue is ScrollViewer oldScrollViewer)
{
calendarControl.DeregisterScrollChanges(oldScrollViewer);
}
if (e.NewValue is ScrollViewer newScrollViewer)
{
calendarControl.RegisterScrollChanges(newScrollViewer);
}
}
}
private static void OnActiveCanvasChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
{
if (calendar is WinoCalendarControl calendarControl)
@@ -128,6 +154,23 @@ namespace Wino.Calendar.Controls
canvas.TimelineCellUnselected += ActiveTimelineCellUnselected;
}
private void RegisterScrollChanges(ScrollViewer scrollViewer)
{
if (scrollViewer == null) return;
scrollViewer.ViewChanging += ScrollViewChanging;
}
private void DeregisterScrollChanges(ScrollViewer scrollViewer)
{
if (scrollViewer == null) return;
scrollViewer.ViewChanging -= ScrollViewChanging;
}
private void ScrollViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
=> ScrollPositionChanging?.Invoke(this, EventArgs.Empty);
private void CalendarSizeChanged(object sender, SizeChangedEventArgs e)
{
if (ActiveCanvas == null) return;
@@ -159,6 +202,22 @@ namespace Wino.Calendar.Controls
public void NavigateToDay(DateTime dateTime) => InternalFlipView.NavigateToDay(dateTime);
public async void NavigateToHour(TimeSpan timeSpan)
{
if (ActiveScrollViewer == null) return;
// Total height of the FlipViewItem is the same as vertical ScrollViewer to position day headers.
await Task.Yield();
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () =>
{
double hourHeght = 60;
double totalHeight = ActiveScrollViewer.ScrollableHeight;
double scrollPosition = timeSpan.TotalHours * hourHeght;
ActiveScrollViewer.ChangeView(null, scrollPosition, null, disableAnimation: false);
});
}
public void ResetTimelineSelection()
{
if (ActiveCanvas == null) return;
@@ -180,6 +239,13 @@ namespace Wino.Calendar.Controls
InternalFlipView.GoPreviousFlip();
}
public void UnselectActiveTimelineCell()
{
if (ActiveCanvas == null) return;
ActiveCanvas.SelectedDateTime = null;
}
public CalendarItemControl GetCalendarItemControl(CalendarItemViewModel calendarItemViewModel)
{
if (ActiveCanvas == null) return null;

View File

@@ -14,13 +14,29 @@ namespace Wino.Calendar.Controls
{
public static readonly DependencyProperty IsIdleProperty = DependencyProperty.Register(nameof(IsIdle), typeof(bool), typeof(WinoCalendarFlipView), new PropertyMetadata(true));
public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarFlipView), new PropertyMetadata(null));
public static readonly DependencyProperty ActiveVerticalScrollViewerProperty = DependencyProperty.Register(nameof(ActiveVerticalScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarFlipView), new PropertyMetadata(null));
/// <summary>
/// Gets or sets the active canvas that is currently displayed in the flip view.
/// Each day-range of flip view item has a canvas that displays the day timeline.
/// </summary>
public WinoDayTimelineCanvas ActiveCanvas
{
get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); }
set { SetValue(ActiveCanvasProperty, value); }
}
/// <summary>
/// Gets or sets the scroll viewer that is currently active in the flip view.
/// It's the vertical scroll that scrolls the timeline only, not the header part that belongs
/// to parent FlipView control.
/// </summary>
public ScrollViewer ActiveVerticalScrollViewer
{
get { return (ScrollViewer)GetValue(ActiveVerticalScrollViewerProperty); }
set { SetValue(ActiveVerticalScrollViewerProperty, value); }
}
public bool IsIdle
{
get { return (bool)GetValue(IsIdleProperty); }
@@ -46,6 +62,7 @@ namespace Wino.Calendar.Controls
if (d is WinoCalendarFlipView flipView)
{
flipView.UpdateActiveCanvas();
flipView.UpdateActiveScrollViewer();
}
}
@@ -62,22 +79,58 @@ namespace Wino.Calendar.Controls
IsIdle = e.Action == NotifyCollectionChangedAction.Reset || e.Action == NotifyCollectionChangedAction.Replace;
}
public async void UpdateActiveCanvas()
private async Task<FlipViewItem> GetCurrentFlipViewItem()
{
// TODO: Refactor this mechanism by listening to PrepareContainerForItemOverride and Loaded events together.
while (ContainerFromIndex(SelectedIndex) == null)
{
await Task.Delay(100);
}
return ContainerFromIndex(SelectedIndex) as FlipViewItem;
}
private void UpdateActiveScrollViewer()
{
if (SelectedIndex < 0)
ActiveVerticalScrollViewer = null;
else
{
GetCurrentFlipViewItem().ContinueWith(task =>
{
if (task.IsCompletedSuccessfully)
{
var flipViewItem = task.Result;
_ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
ActiveVerticalScrollViewer = flipViewItem.FindDescendant<ScrollViewer>();
});
}
});
}
}
public void UpdateActiveCanvas()
{
if (SelectedIndex < 0)
ActiveCanvas = null;
else
{
// TODO: Refactor this mechanism by listening to PrepareContainerForItemOverride and Loaded events together.
while (ContainerFromIndex(SelectedIndex) == null)
GetCurrentFlipViewItem().ContinueWith(task =>
{
await Task.Delay(100);
}
if (task.IsCompletedSuccessfully)
{
var flipViewItem = task.Result;
if (ContainerFromIndex(SelectedIndex) is FlipViewItem flipViewItem)
{
ActiveCanvas = flipViewItem.FindDescendant<WinoDayTimelineCanvas>();
}
_ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
ActiveCanvas = flipViewItem.FindDescendant<WinoDayTimelineCanvas>();
});
}
});
}
}
@@ -127,12 +180,6 @@ namespace Wino.Calendar.Controls
});
}
public void NavigateHour(TimeSpan hourTimeSpan)
{
// Total height of the FlipViewItem is the same as vertical ScrollViewer to position day headers.
// Find the day range that contains the hour.
}
private ObservableRangeCollection<DayRangeRenderModel> GetItemsSource()
=> ItemsSource as ObservableRangeCollection<DayRangeRenderModel>;
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using CommunityToolkit.WinUI;
using Itenso.TimePeriod;
@@ -79,7 +78,7 @@ namespace Wino.Calendar.Controls
var periodRelation = child.Period.GetRelation(Period);
Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}");
// Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}");
if (!child.IsMultiDayEvent)
{
@@ -169,6 +168,15 @@ namespace Wino.Calendar.Controls
if (childWidth < 0) childWidth = 1;
// Regular events must have 2px margin
if (!child.IsMultiDayEvent && !child.IsAllDayEvent)
{
childLeft += 2;
childTop += 2;
childHeight -= 2;
childWidth -= 2;
}
var arrangementRect = new Rect(childLeft + EventItemMargin.Left, childTop + EventItemMargin.Top, Math.Max(childWidth - extraRightMargin, 1), childHeight);
// Make sure measured size will fit in the arranged box.
@@ -179,6 +187,7 @@ namespace Wino.Calendar.Controls
//Debug.WriteLine($"{child.Title}, Measured: {measureSize}, Arranged: {arrangementRect}");
}
return finalSize;
}

View File

@@ -145,8 +145,8 @@ namespace Wino.Calendar.Controls
}
else
{
TimelineCellSelected?.Invoke(this, new TimelineCellSelectedArgs(clickedDateTime, touchPoint, positionerPoint, cellSize));
SelectedDateTime = clickedDateTime;
TimelineCellSelected?.Invoke(this, new TimelineCellSelectedArgs(clickedDateTime, touchPoint, positionerPoint, cellSize));
}
Debug.WriteLine($"Clicked: {clickedDateTime}");

View File

@@ -1,6 +1,11 @@
using System.Linq;
using Windows.UI.Xaml.Controls.Primitives;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain;
using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
using Wino.Helpers;
namespace Wino.Calendar.Helpers
{
@@ -8,5 +13,37 @@ namespace Wino.Calendar.Helpers
{
public static CalendarItemViewModel GetFirstAllDayEvent(CalendarEventCollection collection)
=> (CalendarItemViewModel)collection.AllDayEvents.FirstOrDefault();
public static string GetDetailsPopupDurationString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
{
if (calendarItemViewModel == null || settings == null) return string.Empty;
// Single event in a day.
if (!calendarItemViewModel.IsAllDayEvent && !calendarItemViewModel.IsMultiDayEvent)
{
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} {settings.GetTimeString(calendarItemViewModel.Period.Duration)}";
}
else if (calendarItemViewModel.IsMultiDayEvent)
{
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} - {calendarItemViewModel.Period.End.ToString("d", settings.CultureInfo)}";
}
else
{
// All day event.
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} ({Translator.CalendarItemAllDay})";
}
}
public static PopupPlacementMode GetDesiredPlacementModeForEventsDetailsPopup(
CalendarItemViewModel calendarItemViewModel,
CalendarDisplayType calendarDisplayType)
{
if (calendarItemViewModel == null) return PopupPlacementMode.Auto;
// All and/or multi day events always go to the top of the screen.
if (calendarItemViewModel.IsAllDayEvent || calendarItemViewModel.IsMultiDayEvent) return PopupPlacementMode.Bottom;
return XamlHelpers.GetPlaccementModeForCalendarType(calendarDisplayType);
}
}
}

View File

@@ -20,7 +20,7 @@
<Identity
Name="58272BurakKSE.WinoCalendar"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="1.0.10.0" />
Version="1.0.13.0" />
<mp:PhoneIdentity PhoneProductId="f047b7dd-96ec-4d54-a862-9321e271e449" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

View File

@@ -18,11 +18,6 @@ namespace Wino.Calendar.Services
public event EventHandler<GroupedAccountCalendarViewModel> CollectiveAccountGroupSelectionStateChanged;
public event EventHandler<AccountCalendarViewModel> AccountCalendarSelectionStateChanged;
[ObservableProperty]
public ObservableCollection<CalendarItemViewModel> _selectedItems = new ObservableCollection<CalendarItemViewModel>();
public bool HasMultipleSelectedItems => SelectedItems.Count > 1;
[ObservableProperty]
private ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> groupedAccountCalendars;
@@ -52,13 +47,6 @@ namespace Wino.Calendar.Services
public AccountCalendarStateService()
{
GroupedAccountCalendars = new ReadOnlyObservableCollection<GroupedAccountCalendarViewModel>(_internalGroupedAccountCalendars);
SelectedItems.CollectionChanged += SelectedCalendarItemsUpdated;
}
private void SelectedCalendarItemsUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(HasMultipleSelectedItems));
}
private void SingleGroupCalendarCollectiveStateChanged(object sender, EventArgs e)

View File

@@ -9,6 +9,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:Wino.Core.Domain.Models.Calendar"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
xmlns:selectors="using:Wino.Calendar.Selectors"
xmlns:toolkitControls="using:CommunityToolkit.WinUI.Controls">
@@ -90,8 +91,6 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Rendering left hour headers. -->
<ItemsControl ItemTemplate="{StaticResource DayCalendarHourHeaderTemplate}" ItemsSource="{x:Bind DayHeaders}" />
@@ -138,6 +137,7 @@
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
ActiveCanvas="{x:Bind ActiveCanvas, Mode=TwoWay}"
ActiveVerticalScrollViewer="{x:Bind ActiveScrollViewer, Mode=TwoWay}"
Background="Transparent"
IsIdle="{x:Bind IsFlipIdle, Mode=TwoWay}"
IsTabStop="False"
@@ -220,11 +220,42 @@
<StackPanel Grid.Column="1" HorizontalAlignment="Right" />
<!-- All-Multi Day Events -->
<controls:AllDayItemsControl
<ItemsControl
x:Name="PART_AllDayItemsControl"
Grid.Row="1"
Grid.ColumnSpan="2"
Margin="0,6" />
Margin="0,6">
<ItemsControl.ItemTemplateSelector>
<selectors:CustomAreaCalendarItemSelector>
<selectors:CustomAreaCalendarItemSelector.AllDayTemplate>
<DataTemplate x:DataType="data:CalendarItemViewModel">
<controls:CalendarItemControl
CalendarItem="{x:Bind}"
DisplayingDate="{Binding DataContext, ElementName=PART_AllDayItemsControl}"
IsCustomEventArea="True" />
</DataTemplate>
</selectors:CustomAreaCalendarItemSelector.AllDayTemplate>
<selectors:CustomAreaCalendarItemSelector.MultiDayTemplate>
<DataTemplate x:DataType="data:CalendarItemViewModel">
<controls:CalendarItemControl
CalendarItem="{x:Bind}"
DisplayingDate="{Binding DataContext, ElementName=PART_AllDayItemsControl}"
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>
</Grid>
<VisualStateManager.VisualStateGroups>

View File

@@ -150,7 +150,11 @@
Grid.RowSpan="2"
Grid.ColumnSpan="2"
Background="{ThemeResource WinoApplicationBackgroundColor}"
IsHitTestVisible="False" />
IsHitTestVisible="False">
<Grid.BackgroundTransition>
<BrushTransition />
</Grid.BackgroundTransition>
</Grid>
<SplitView
x:Name="MainSplitView"
@@ -236,7 +240,7 @@
CornerRadius="3">
<TextBlock
FontSize="14"
Foreground="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(TextColorHex), Mode=OneWay}"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(BackgroundColorHex), Mode=OneWay}"
Text="{x:Bind Name, Mode=OneWay}"
TextWrapping="Wrap" />
</Border>

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,6 @@ using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Navigation;
using Wino.Calendar.Args;
using Wino.Calendar.ViewModels.Messages;
using Wino.Calendar.Views.Abstract;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
@@ -14,9 +13,9 @@ namespace Wino.Calendar.Views
{
public sealed partial class CalendarPage : CalendarPageAbstract,
IRecipient<ScrollToDateMessage>,
IRecipient<ScrollToHourMessage>,
IRecipient<GoNextDateRequestedMessage>,
IRecipient<GoPreviousDateRequestedMessage>,
IRecipient<CalendarItemRightTappedMessage>
IRecipient<GoPreviousDateRequestedMessage>
{
private const int PopupDialogOffset = 12;
@@ -24,12 +23,26 @@ namespace Wino.Calendar.Views
{
InitializeComponent();
NavigationCacheMode = NavigationCacheMode.Enabled;
ViewModel.DetailsShowCalendarItemChanged += CalendarItemDetailContextChanged;
}
private void CalendarItemDetailContextChanged(object sender, EventArgs e)
{
if (ViewModel.DisplayDetailsCalendarItemViewModel != null)
{
var control = CalendarControl.GetCalendarItemControl(ViewModel.DisplayDetailsCalendarItemViewModel);
if (control != null)
{
EventDetailsPopup.PlacementTarget = control;
}
}
}
public void Receive(ScrollToHourMessage message) => CalendarControl.NavigateToHour(message.TimeSpan);
public void Receive(ScrollToDateMessage message) => CalendarControl.NavigateToDay(message.Date);
public void Receive(GoNextDateRequestedMessage message) => CalendarControl.GoNextRange();
public void Receive(GoPreviousDateRequestedMessage message) => CalendarControl.GoPreviousRange();
protected override void OnNavigatedTo(NavigationEventArgs e)
@@ -53,6 +66,17 @@ namespace Wino.Calendar.Views
private void CellSelected(object sender, TimelineCellSelectedArgs e)
{
// Dismiss event details if exists and cancel the selection.
// This is to prevent the event details from being displayed when the user clicks somewhere else.
if (EventDetailsPopup.IsOpen)
{
CalendarControl.UnselectActiveTimelineCell();
ViewModel.DisplayDetailsCalendarItemViewModel = null;
return;
}
ViewModel.SelectedQuickEventDate = e.ClickedDate;
TeachingTipPositionerGrid.Width = e.CellSize.Width;
@@ -115,15 +139,23 @@ namespace Wino.Calendar.Views
}
public void Receive(CalendarItemRightTappedMessage message)
{
}
private void StartTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
=> ViewModel.SelectedStartTimeString = args.Text;
private void EndTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
=> ViewModel.SelectedEndTimeString = args.Text;
private void EventDetailsPopupClosed(object sender, object e)
{
ViewModel.DisplayDetailsCalendarItemViewModel = null;
}
private void CalendarScrolling(object sender, EventArgs e)
{
// In case of scrolling, we must dismiss the event details dialog.
ViewModel.DisplayDetailsCalendarItemViewModel = null;
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -137,9 +137,6 @@
</Compile>
<Compile Include="Args\TimelineCellSelectedArgs.cs" />
<Compile Include="Args\TimelineCellUnselectedArgs.cs" />
<Compile Include="Controls\AllDayItemsControl.xaml.cs">
<DependentUpon>AllDayItemsControl.xaml</DependentUpon>
</Compile>
<Compile Include="Controls\CalendarItemControl.xaml.cs">
<DependentUpon>CalendarItemControl.xaml</DependentUpon>
</Compile>
@@ -252,10 +249,6 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Page Include="Controls\AllDayItemsControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Controls\CalendarItemControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>

View File

@@ -43,6 +43,17 @@ namespace Wino.Core.Domain.Collections
return _allItems.FirstOrDefault(x => x.Id == calendarItemId);
}
public void ClearSelectionStates()
{
foreach (var item in _allItems)
{
if (item is ICalendarItemViewModel calendarItemViewModel)
{
calendarItemViewModel.IsSelected = false;
}
}
}
public void FilterByCalendars(IEnumerable<Guid> visibleCalendarIds)
{
foreach (var item in _allItems)
@@ -75,14 +86,7 @@ namespace Wino.Core.Domain.Collections
}
else if (calendarItem.IsMultiDayEvent)
{
if (Settings.GhostRenderAllDayItems)
{
return [_internalRegularEvents, _internalAllDayEvents];
}
else
{
return [_internalAllDayEvents];
}
return [_internalRegularEvents, _internalAllDayEvents];
}
else
{

View File

@@ -14,6 +14,10 @@ namespace Wino.Core.Domain.Entities.Calendar
public string Name { get; set; }
public bool IsPrimary { get; set; }
public bool IsExtended { get; set; } = true;
/// <summary>
/// Unused for now.
/// </summary>
public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; }
public string TimeZone { get; set; }

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using Itenso.TimePeriod;
using SQLite;
using Wino.Core.Domain.Enums;
@@ -6,6 +7,7 @@ using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Entities.Calendar
{
[DebuggerDisplay("{Title} ({StartDate} - {EndDate})")]
public class CalendarItem : ICalendarItem
{
[PrimaryKey]
@@ -52,6 +54,29 @@ namespace Wino.Core.Domain.Entities.Calendar
}
}
/// <summary>
/// Events that are either an exceptional instance of a recurring event or a recurring event itself.
/// </summary>
public bool IsRecurringEvent
{
get
{
return !string.IsNullOrEmpty(Recurrence) || RecurringCalendarItemId != null;
}
}
/// <summary>
/// Events that are belong to parent recurring event, but updated individually are considered single exceptional instances.
/// They will have different Id of their own.
/// </summary>
public bool IsSingleExceptionalInstance
{
get
{
return RecurringCalendarItemId != null;
}
}
/// <summary>
/// Events that are not all-day events and last more than one day are considered multi-day events.
/// </summary>
@@ -66,10 +91,13 @@ namespace Wino.Core.Domain.Entities.Calendar
public double DurationInSeconds { get; set; }
public string Recurrence { get; set; }
public string OrganizerDisplayName { get; set; }
public string OrganizerEmail { 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
/// This makes the calendar item a child of the recurring event.
/// </summary>
public Guid? RecurringCalendarItemId { get; set; }
@@ -80,7 +108,7 @@ namespace Wino.Core.Domain.Entities.Calendar
/// <summary>
/// Hidden events must not be displayed to the user.
/// This usually happens when a child instance of recurring parent hapens.
/// This usually happens when a child instance of recurring parent is cancelled after creation.
/// </summary>
public bool IsHidden { get; set; }

View File

@@ -2,6 +2,7 @@
{
public enum CalendarItemStatus
{
NotResponded,
Confirmed,
Tentative,
Cancelled,

View File

@@ -12,7 +12,10 @@ namespace Wino.Core.Domain.Interfaces
DateTime EndDate { get; }
double DurationInSeconds { get; set; }
ITimePeriod Period { get; }
bool IsRecurringEvent { get; }
bool IsAllDayEvent { get; }
bool IsMultiDayEvent { get; }
bool IsSingleExceptionalInstance { get; }
}
}

View File

@@ -3,5 +3,8 @@
/// <summary>
/// Temporarily to enforce CalendarItemViewModel. Used in CalendarEventCollection.
/// </summary>
public interface ICalendarItemViewModel { }
public interface ICalendarItemViewModel
{
bool IsSelected { get; set; }
}
}

View File

@@ -187,7 +187,6 @@ namespace Wino.Core.Domain.Interfaces
DayOfWeek WorkingDayStart { get; set; }
DayOfWeek WorkingDayEnd { get; set; }
double HourHeight { get; set; }
bool GhostRenderAllDayEvents { get; set; }
CalendarSettings GetCurrentCalendarSettings();

View File

@@ -11,8 +11,7 @@ namespace Wino.Core.Domain.Models.Calendar
TimeSpan WorkingHourEnd,
double HourHeight,
DayHeaderDisplayType DayHeaderDisplayType,
CultureInfo CultureInfo,
bool GhostRenderAllDayItems)
CultureInfo CultureInfo)
{
public TimeSpan? GetTimeSpan(string selectedTime)
{

View File

@@ -13,6 +13,8 @@ namespace Wino.Core.Domain.Models.Calendar
{
public ITimePeriod Period { get; }
public List<CalendarDayModel> CalendarDays { get; } = [];
// TODO: Get rid of this at some point.
public List<DayHeaderRenderModel> DayHeaders { get; } = [];
public CalendarRenderOptions CalendarRenderOptions { get; }
@@ -46,12 +48,5 @@ namespace Wino.Core.Domain.Models.Calendar
DayHeaders.Add(new DayHeaderRenderModel(dayHeader, calendarRenderOptions.CalendarSettings.HourHeight));
}
}
//public void AddEvent(ICalendarItem calendarEventModel)
//{
// var calendarDayModel = CalendarDays.FirstOrDefault(x => x.Period.HasInside(calendarEventModel.Period.Start));
// calendarDayModel?.EventsCollection.AddCalendarItem(calendarEventModel);
//}
}
}

View File

@@ -132,6 +132,9 @@
"Dialog_DontAskAgain": "Don't ask again",
"CalendarAllDayEventSummary": "all-day events",
"CalendarItemAllDay": "all day",
"CalendarItem_DetailsPopup_JoinOnline": "Join online",
"CalendarItem_DetailsPopup_ViewEventButton": "View event",
"CalendarItem_DetailsPopup_ViewSeriesButton": "View series",
"CreateAccountAliasDialog_Title": "Create Account Alias",
"CreateAccountAliasDialog_Description": "Make sure your outgoing server allows sending mails from this alias.",
"CreateAccountAliasDialog_AliasAddress": "Address",
@@ -635,3 +638,4 @@
"QuickEventDialog_EventName": "Event name",
"QuickEventDialog_IsAllDay": "All day"
}

View File

@@ -36,6 +36,8 @@ namespace Wino.Helpers
}
public static Visibility ReverseBoolToVisibilityConverter(bool value) => value ? Visibility.Collapsed : Visibility.Visible;
public static Visibility ReverseVisibilityConverter(Visibility visibility) => visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
public static bool ReverseBoolConverter(bool value) => !value;

View File

@@ -265,12 +265,6 @@ namespace Wino.Core.UWP.Services
set => SaveProperty(propertyName: nameof(WorkingDayEnd), value);
}
public bool GhostRenderAllDayEvents
{
get => _configurationService.Get(nameof(GhostRenderAllDayEvents), true);
set => SaveProperty(nameof(GhostRenderAllDayEvents), value);
}
public CalendarSettings GetCurrentCalendarSettings()
{
var workingDays = GetDaysBetween(WorkingDayStart, WorkingDayEnd);
@@ -281,8 +275,7 @@ namespace Wino.Core.UWP.Services
WorkingHourEnd,
HourHeight,
Prefer24HourTimeFormat ? DayHeaderDisplayType.TwentyFourHour : DayHeaderDisplayType.TwelveHour,
new CultureInfo(WinoTranslationDictionary.GetLanguageFileNameRelativePath(CurrentLanguage)),
GhostRenderAllDayEvents);
new CultureInfo(WinoTranslationDictionary.GetLanguageFileNameRelativePath(CurrentLanguage)));
}
private List<DayOfWeek> GetDaysBetween(DayOfWeek startDay, DayOfWeek endDay)

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Web;
using CommunityToolkit.Diagnostics;
using Google.Apis.Calendar.v3.Data;
using Google.Apis.Gmail.v1.Data;
using MimeKit;
@@ -182,22 +181,11 @@ namespace Wino.Core.Extensions
IsPrimary = calendarListEntry.Primary.GetValueOrDefault(),
};
// Optional background color.
if (calendarListEntry.BackgroundColor != null) calendar.BackgroundColorHex = calendarListEntry.BackgroundColor;
// Bg color must present. Generate one if doesnt exists.
// Text color is optional. It'll be overriden by UI for readibility.
if (!string.IsNullOrEmpty(calendarListEntry.ForegroundColor))
{
calendar.TextColorHex = calendarListEntry.ForegroundColor;
}
else
{
// Calendars must have text color assigned.
// Generate one if not provided.
var randomColor = RandomFlatColorGenerator.Generate();
calendar.TextColorHex = randomColor.ToHexString();
}
calendar.BackgroundColorHex = string.IsNullOrEmpty(calendarListEntry.BackgroundColor) ? ColorHelpers.GenerateFlatColorHex() : calendar.BackgroundColorHex;
calendar.TextColorHex = string.IsNullOrEmpty(calendarListEntry.ForegroundColor) ? "#000000" : calendarListEntry.ForegroundColor;
return calendar;
}

View File

@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Graph.Models;
using MimeKit;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Misc;
namespace Wino.Core.Extensions
{
@@ -101,6 +105,148 @@ namespace Wino.Core.Extensions
return message;
}
public static AccountCalendar AsCalendar(this Calendar outlookCalendar, MailAccount assignedAccount)
{
var calendar = new AccountCalendar()
{
AccountId = assignedAccount.Id,
Id = Guid.NewGuid(),
RemoteCalendarId = outlookCalendar.Id,
IsPrimary = outlookCalendar.IsDefaultCalendar.GetValueOrDefault(),
Name = outlookCalendar.Name,
IsExtended = true,
};
// Colors:
// Bg must be present. Generate flat one if doesn't exists.
// Text doesnt exists for Outlook.
calendar.BackgroundColorHex = string.IsNullOrEmpty(outlookCalendar.HexColor) ? ColorHelpers.GenerateFlatColorHex() : outlookCalendar.HexColor;
calendar.TextColorHex = "#000000";
return calendar;
}
private static string GetRfc5545DayOfWeek(DayOfWeekObject dayOfWeek)
{
return dayOfWeek switch
{
DayOfWeekObject.Monday => "MO",
DayOfWeekObject.Tuesday => "TU",
DayOfWeekObject.Wednesday => "WE",
DayOfWeekObject.Thursday => "TH",
DayOfWeekObject.Friday => "FR",
DayOfWeekObject.Saturday => "SA",
DayOfWeekObject.Sunday => "SU",
_ => throw new ArgumentOutOfRangeException(nameof(dayOfWeek), dayOfWeek, null)
};
}
public static string ToRfc5545RecurrenceString(this PatternedRecurrence recurrence)
{
if (recurrence == null || recurrence.Pattern == null)
throw new ArgumentNullException(nameof(recurrence), "PatternedRecurrence or its Pattern cannot be null.");
var ruleBuilder = new StringBuilder("RRULE:");
var pattern = recurrence.Pattern;
// Frequency
switch (pattern.Type)
{
case RecurrencePatternType.Daily:
ruleBuilder.Append("FREQ=DAILY;");
break;
case RecurrencePatternType.Weekly:
ruleBuilder.Append("FREQ=WEEKLY;");
break;
case RecurrencePatternType.AbsoluteMonthly:
ruleBuilder.Append("FREQ=MONTHLY;");
break;
case RecurrencePatternType.AbsoluteYearly:
ruleBuilder.Append("FREQ=YEARLY;");
break;
case RecurrencePatternType.RelativeMonthly:
ruleBuilder.Append("FREQ=MONTHLY;");
break;
case RecurrencePatternType.RelativeYearly:
ruleBuilder.Append("FREQ=YEARLY;");
break;
default:
throw new NotSupportedException($"Unsupported recurrence pattern type: {pattern.Type}");
}
// Interval
if (pattern.Interval > 0)
ruleBuilder.Append($"INTERVAL={pattern.Interval};");
// Days of Week
if (pattern.DaysOfWeek?.Any() == true)
{
var days = string.Join(",", pattern.DaysOfWeek.Select(day => day.ToString().ToUpperInvariant().Substring(0, 2)));
ruleBuilder.Append($"BYDAY={days};");
}
// Day of Month (BYMONTHDAY)
if (pattern.Type == RecurrencePatternType.AbsoluteMonthly || pattern.Type == RecurrencePatternType.AbsoluteYearly)
{
if (pattern.DayOfMonth <= 0)
throw new ArgumentException("DayOfMonth must be greater than 0 for absoluteMonthly or absoluteYearly patterns.");
ruleBuilder.Append($"BYMONTHDAY={pattern.DayOfMonth};");
}
// Month (BYMONTH)
if (pattern.Type == RecurrencePatternType.AbsoluteYearly || pattern.Type == RecurrencePatternType.RelativeYearly)
{
if (pattern.Month <= 0)
throw new ArgumentException("Month must be greater than 0 for absoluteYearly or relativeYearly patterns.");
ruleBuilder.Append($"BYMONTH={pattern.Month};");
}
// Count or Until
if (recurrence.Range != null)
{
if (recurrence.Range.Type == RecurrenceRangeType.EndDate && recurrence.Range.EndDate != null)
{
ruleBuilder.Append($"UNTIL={recurrence.Range.EndDate.Value:yyyyMMddTHHmmssZ};");
}
else if (recurrence.Range.Type == RecurrenceRangeType.Numbered && recurrence.Range.NumberOfOccurrences.HasValue)
{
ruleBuilder.Append($"COUNT={recurrence.Range.NumberOfOccurrences.Value};");
}
}
// Remove trailing semicolon
return ruleBuilder.ToString().TrimEnd(';');
}
public static DateTimeOffset GetDateTimeOffsetFromDateTimeTimeZone(DateTimeTimeZone dateTimeTimeZone)
{
if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime) || string.IsNullOrEmpty(dateTimeTimeZone.TimeZone))
{
throw new ArgumentException("DateTimeTimeZone is null or empty.");
}
try
{
// Parse the DateTime string
if (DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime))
{
// Get TimeZoneInfo to get the offset
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(dateTimeTimeZone.TimeZone);
TimeSpan offset = timeZoneInfo.GetUtcOffset(parsedDateTime);
return new DateTimeOffset(parsedDateTime, offset);
}
else
throw new ArgumentException("DateTime string is not in a valid format.");
}
catch (Exception)
{
throw;
}
}
#region Mime to Outlook Message Helpers
private static IEnumerable<Recipient> GetRecipients(this InternetAddressList internetAddresses)
@@ -176,6 +322,8 @@ namespace Wino.Core.Extensions
return id;
}
#endregion
}
}

View File

@@ -103,6 +103,7 @@ namespace Wino.Core.Integration.Processors
/// <param name="accountId">Account identifier to reset delta token for.</param>
/// <returns>Empty string to assign account delta sync for.</returns>
Task<string> ResetAccountDeltaTokenAsync(Guid accountId);
Task ManageCalendarEventAsync(Microsoft.Graph.Models.Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount);
}
public interface IImapChangeProcessor : IDefaultChangeProcessor

View File

@@ -85,6 +85,10 @@ namespace Wino.Core.Integration.Processors
totalDurationInSeconds = parentRecurringEvent.DurationInSeconds;
}
var organizerMail = GetOrganizerEmail(calendarEvent, organizerAccount);
var organizerName = GetOrganizerName(calendarEvent, organizerAccount);
calendarItem = new CalendarItem()
{
CalendarId = assignedCalendar.Id,
@@ -103,9 +107,11 @@ namespace Wino.Core.Integration.Processors
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,
HtmlLink = string.IsNullOrEmpty(calendarEvent.HtmlLink) ? parentRecurringEvent.HtmlLink : calendarEvent.HtmlLink,
RemoteEventId = calendarEvent.Id,
IsLocked = calendarEvent.Locked.GetValueOrDefault()
IsLocked = calendarEvent.Locked.GetValueOrDefault(),
OrganizerDisplayName = string.IsNullOrEmpty(organizerName) ? parentRecurringEvent.OrganizerDisplayName : organizerName,
OrganizerEmail = string.IsNullOrEmpty(organizerMail) ? parentRecurringEvent.OrganizerEmail : organizerMail
};
}
else
@@ -137,7 +143,9 @@ namespace Wino.Core.Integration.Processors
Visibility = GetVisibility(calendarEvent.Visibility),
HtmlLink = calendarEvent.HtmlLink,
RemoteEventId = calendarEvent.Id,
IsLocked = calendarEvent.Locked.GetValueOrDefault()
IsLocked = calendarEvent.Locked.GetValueOrDefault(),
OrganizerDisplayName = GetOrganizerName(calendarEvent, organizerAccount),
OrganizerEmail = GetOrganizerEmail(calendarEvent, organizerAccount)
};
}
@@ -216,8 +224,6 @@ namespace Wino.Core.Integration.Processors
// We have this event already. Update it.
if (calendarEvent.Status == "cancelled")
{
// Event is canceled.
// Parent event is canceled. We must delete everything.
if (string.IsNullOrEmpty(recurringEventId))
{
@@ -249,6 +255,30 @@ namespace Wino.Core.Integration.Processors
await Connection.InsertOrReplaceAsync(existingCalendarItem);
}
private string GetOrganizerName(Event calendarEvent, MailAccount account)
{
if (calendarEvent.Organizer == null) return string.Empty;
if (calendarEvent.Organizer.Self == true)
{
return account.SenderName;
}
else
return calendarEvent.Organizer.DisplayName;
}
private string GetOrganizerEmail(Event calendarEvent, MailAccount account)
{
if (calendarEvent.Organizer == null) return string.Empty;
if (calendarEvent.Organizer.Self == true)
{
return account.Address;
}
else
return calendarEvent.Organizer.Email;
}
private CalendarItemStatus GetStatus(string status)
{
return status switch

View File

@@ -1,6 +1,12 @@
using System;
using System.Threading.Tasks;
using Microsoft.Graph.Models;
using Serilog;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Extensions;
using Wino.Services;
namespace Wino.Core.Integration.Processors
@@ -35,5 +41,105 @@ namespace Wino.Core.Integration.Processors
public Task UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string synchronizationIdentifier)
=> Connection.ExecuteAsync("UPDATE MailItemFolder SET DeltaToken = ? WHERE Id = ?", synchronizationIdentifier, folderId);
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
{
// TODO: Make sure to call this method ordered by type:SeriesMaster first.
// otherwise we might lose exceptions.s
// We parse the occurrences based on the parent event.
// There is literally no point to store them because
// type=Exception events are the exceptional childs of recurrency parent event.
if (calendarEvent.Type == EventType.Occurrence) return;
var savingItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id);
Guid savingItemId = Guid.Empty;
if (savingItem != null)
savingItemId = savingItem.Id;
else
{
savingItemId = Guid.NewGuid();
savingItem = new CalendarItem() { Id = savingItemId };
}
DateTimeOffset eventStartDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.Start);
DateTimeOffset eventEndDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.End);
var durationInSeconds = (eventEndDateTimeOffset - eventStartDateTimeOffset).TotalSeconds;
savingItem.RemoteEventId = calendarEvent.Id;
savingItem.StartDate = eventStartDateTimeOffset.DateTime;
savingItem.StartDateOffset = eventStartDateTimeOffset.Offset;
savingItem.EndDateOffset = eventEndDateTimeOffset.Offset;
savingItem.DurationInSeconds = durationInSeconds;
savingItem.Title = calendarEvent.Subject;
savingItem.Description = calendarEvent.Body?.Content;
savingItem.Location = calendarEvent.Location?.DisplayName;
if (calendarEvent.Type == EventType.Exception && !string.IsNullOrEmpty(calendarEvent.SeriesMasterId))
{
// This is a recurring event exception.
// We need to find the parent event and set it as recurring event id.
var parentEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.SeriesMasterId);
if (parentEvent != null)
{
savingItem.RecurringCalendarItemId = parentEvent.Id;
}
else
{
Log.Warning($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}");
return;
}
}
// Convert the recurrence pattern to string for parent recurring events.
if (calendarEvent.Type == EventType.SeriesMaster && calendarEvent.Recurrence != null)
{
savingItem.Recurrence = OutlookIntegratorExtensions.ToRfc5545RecurrenceString(calendarEvent.Recurrence);
}
savingItem.HtmlLink = calendarEvent.WebLink;
savingItem.CalendarId = assignedCalendar.Id;
savingItem.OrganizerEmail = calendarEvent.Organizer?.EmailAddress?.Address;
savingItem.OrganizerDisplayName = calendarEvent.Organizer?.EmailAddress?.Name;
savingItem.IsHidden = false;
if (calendarEvent.ResponseStatus?.Response != null)
{
switch (calendarEvent.ResponseStatus.Response.Value)
{
case ResponseType.None:
case ResponseType.NotResponded:
savingItem.Status = CalendarItemStatus.NotResponded;
break;
case ResponseType.TentativelyAccepted:
savingItem.Status = CalendarItemStatus.Tentative;
break;
case ResponseType.Accepted:
case ResponseType.Organizer:
savingItem.Status = CalendarItemStatus.Confirmed;
break;
case ResponseType.Declined:
savingItem.Status = CalendarItemStatus.Cancelled;
savingItem.IsHidden = true;
break;
default:
break;
}
}
else
{
savingItem.Status = CalendarItemStatus.Confirmed;
}
// Upsert the event.
await Connection.InsertOrReplaceAsync(savingItem);
}
}
}

View File

@@ -3,18 +3,24 @@ using System.Drawing;
namespace Wino.Core.Misc
{
public static class RandomFlatColorGenerator
public static class ColorHelpers
{
public static Color Generate()
public static string GenerateFlatColorHex()
{
Random random = new();
int hue = random.Next(0, 360); // Full hue range
int saturation = 70 + random.Next(30); // High saturation (70-100%)
int lightness = 50 + random.Next(20); // Bright colors (50-70%)
return FromHsl(hue, saturation, lightness);
var color = FromHsl(hue, saturation, lightness);
return ToHexString(color);
}
public static string ToHexString(this Color c) => $"#{c.R:X2}{c.G:X2}{c.B:X2}";
public static string ToRgbString(this Color c) => $"RGB({c.R}, {c.G}, {c.B})";
private static Color FromHsl(int h, int s, int l)
{
double hue = h / 360.0;

View File

@@ -20,6 +20,7 @@ using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
using MimeKit;
using MoreLinq.Extensions;
using Serilog;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
@@ -957,9 +958,177 @@ namespace Wino.Core.Synchronizers.Mail
return [package];
}
protected override Task<CalendarSynchronizationResult> SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default)
protected override async Task<CalendarSynchronizationResult> SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
_logger.Information("Internal calendar synchronization started for {Name}", Account.Name);
cancellationToken.ThrowIfCancellationRequested();
// await SynchronizeCalendarsAsync(cancellationToken).ConfigureAwait(false);
var localCalendars = await _outlookChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
foreach (var calendar in localCalendars)
{
bool isInitialSync = string.IsNullOrEmpty(calendar.SynchronizationDeltaToken);
Microsoft.Graph.Me.CalendarView.Delta.DeltaGetResponse eventsDeltaResponse = null;
if (isInitialSync)
{
_logger.Debug("No sync identifier for Calendar {FolderName}. Performing initial sync.", calendar.Name);
var startDate = DateTime.UtcNow.AddYears(-2).ToString("u");
var endDate = DateTime.UtcNow.ToString("u");
// No delta link. Performing initial sync.
eventsDeltaResponse = await _graphClient.Me.CalendarView.Delta.GetAsDeltaGetResponseAsync((requestConfiguration) =>
{
requestConfiguration.QueryParameters.StartDateTime = startDate;
requestConfiguration.QueryParameters.EndDateTime = endDate;
requestConfiguration.QueryParameters.Expand = ["calendar"];
}, cancellationToken: cancellationToken);
}
else
{
var currentDeltaToken = calendar.SynchronizationDeltaToken;
var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events.Delta.ToGetRequestInformation((config) =>
{
config.QueryParameters.Top = (int)InitialMessageDownloadCountPerFolder;
config.QueryParameters.Select = outlookMessageSelectParameters;
config.QueryParameters.Orderby = ["receivedDateTime desc"];
});
requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken");
requestInformation.QueryParameters.Add("%24deltatoken", currentDeltaToken);
// eventsDeltaResponse = await _graphClient.RequestAdapter.SendAsync(requestInformation, Microsoft.Graph.Me.Calendars.Item.Events.Delta.DeltaGetResponse.CreateFromDiscriminatorValue);
}
List<Event> events = new();
// We must first save the parent recurring events to not lose exceptions.
// Therefore, order the existing items by their type and save the parent recurring events first.
var messageIteratorAsync = PageIterator<Event, Microsoft.Graph.Me.CalendarView.Delta.DeltaGetResponse>.CreatePageIterator(_graphClient, eventsDeltaResponse, (item) =>
{
events.Add(item);
return true;
});
await messageIteratorAsync
.IterateAsync(cancellationToken)
.ConfigureAwait(false);
// Desc-order will move parent recurring events to the top.
events = events.OrderByDescending(a => a.Type).ToList();
foreach (var item in events)
{
try
{
await _handleItemRetrievalSemaphore.WaitAsync();
await _outlookChangeProcessor.ManageCalendarEventAsync(item, calendar, Account).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.Error(ex, "Error occurred while handling item {Id} for calendar {Name}", item.Id, calendar.Name);
}
finally
{
_handleItemRetrievalSemaphore.Release();
}
}
// latestDeltaLink = messageIteratorAsync.Deltalink;
}
return default;
}
private async Task SynchronizeCalendarsAsync(CancellationToken cancellationToken = default)
{
var calendars = await _graphClient.Me.Calendars.GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
var localCalendars = await _outlookChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
List<AccountCalendar> insertedCalendars = new();
List<AccountCalendar> updatedCalendars = new();
List<AccountCalendar> deletedCalendars = new();
// 1. Handle deleted calendars.
foreach (var calendar in localCalendars)
{
var remoteCalendar = calendars.Value.FirstOrDefault(a => a.Id == calendar.RemoteCalendarId);
if (remoteCalendar == null)
{
// Local calendar doesn't exists remotely. Delete local copy.
await _outlookChangeProcessor.DeleteAccountCalendarAsync(calendar).ConfigureAwait(false);
deletedCalendars.Add(calendar);
}
}
// Delete the deleted folders from local list.
deletedCalendars.ForEach(a => localCalendars.Remove(a));
// 2. Handle update/insert based on remote calendars.
foreach (var calendar in calendars.Value)
{
var existingLocalCalendar = localCalendars.FirstOrDefault(a => a.RemoteCalendarId == calendar.Id);
if (existingLocalCalendar == null)
{
// Insert new calendar.
var localCalendar = calendar.AsCalendar(Account);
insertedCalendars.Add(localCalendar);
}
else
{
// Update existing calendar. Right now we only update the name.
if (ShouldUpdateCalendar(calendar, existingLocalCalendar))
{
existingLocalCalendar.Name = calendar.Name;
updatedCalendars.Add(existingLocalCalendar);
}
else
{
// Remove it from the local folder list to skip additional calendar updates.
localCalendars.Remove(existingLocalCalendar);
}
}
}
// 3.Process changes in order-> Insert, Update. Deleted ones are already processed.
foreach (var calendar in insertedCalendars)
{
await _outlookChangeProcessor.InsertAccountCalendarAsync(calendar).ConfigureAwait(false);
}
foreach (var calendar in updatedCalendars)
{
await _outlookChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
}
if (insertedCalendars.Any() || deletedCalendars.Any() || updatedCalendars.Any())
{
// TODO: Notify calendar updates.
// WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(Account.Id));
}
}
private bool ShouldUpdateCalendar(Calendar calendar, AccountCalendar accountCalendar)
{
// TODO: Only calendar name is updated for now. We can add more checks here.
var remoteCalendarName = calendar.Name;
var localCalendarName = accountCalendar.Name;
return !localCalendarName.Equals(remoteCalendarName, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@@ -0,0 +1,10 @@
using System;
namespace Wino.Messaging.Client.Calendar
{
/// <summary>
/// Emitted when vertical scroll position is requested to be changed.
/// </summary>
/// <param name="TimeSpan">Hour to scroll vertically on flip view item.</param>
public record ScrollToHourMessage(TimeSpan TimeSpan);
}

View File

@@ -62,9 +62,9 @@ namespace Wino.Services
List<CalendarItem> eventsToRemove = new() { calendarItem };
// In case of parent event, delete all child events as well.
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);
@@ -97,7 +97,7 @@ namespace Wino.Services
public async Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, DayRangeRenderModel dayRangeRenderModel)
{
// 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 for given calendar.
var accountEvents = await Connection.Table<CalendarItem>()
.Where(x => x.CalendarId == calendar.Id && !x.IsHidden).ToListAsync();
@@ -127,8 +127,8 @@ namespace Wino.Services
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.
// Wino stores exceptional recurrent events as a separate calendar item, without the recurrence rule.
// Because each instance 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.
@@ -150,15 +150,14 @@ namespace Wino.Services
foreach (var occurrence in occurrences)
{
var singleInstance = exceptionalRecurrences.FirstOrDefault(a =>
a.StartDate == occurrence.Period.StartTime.Value &&
a.EndDate == occurrence.Period.EndTime.Value);
var exactInstanceCheck = exceptionalRecurrences.FirstOrDefault(a =>
a.Period.OverlapsWith(dayRangeRenderModel.Period));
if (singleInstance == null)
if (exactInstanceCheck == 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.
// There is no exception for the period.
// Change the instance StartDate and Duration.
ev.StartDate = occurrence.Period.StartTime.Value;
ev.DurationInSeconds = (occurrence.Period.EndTime.Value - occurrence.Period.StartTime.Value).TotalSeconds;