Outlook calendar/event syncing basics without delta. Bunch of UI updates for the calendar view.
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
public enum CalendarItemStatus
|
||||
{
|
||||
NotResponded,
|
||||
Confirmed,
|
||||
Tentative,
|
||||
Cancelled,
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,8 @@
|
||||
/// <summary>
|
||||
/// Temporarily to enforce CalendarItemViewModel. Used in CalendarEventCollection.
|
||||
/// </summary>
|
||||
public interface ICalendarItemViewModel { }
|
||||
public interface ICalendarItemViewModel
|
||||
{
|
||||
bool IsSelected { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
Wino.Messages/Client/Calendar/ScrollToHourMessage.cs
Normal file
10
Wino.Messages/Client/Calendar/ScrollToHourMessage.cs
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user