863 lines
30 KiB
C#
863 lines
30 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Diagnostics;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using MoreLinq;
|
|
using Serilog;
|
|
using Wino.Calendar.ViewModels.Data;
|
|
using Wino.Calendar.ViewModels.Interfaces;
|
|
using Wino.Calendar.ViewModels.Messages;
|
|
using Wino.Core.Domain.Collections;
|
|
using Wino.Core.Domain.Entities.Calendar;
|
|
using Wino.Core.Domain.Enums;
|
|
using Wino.Core.Domain.Interfaces;
|
|
using Wino.Core.Domain.Models.Calendar;
|
|
using Wino.Core.Domain.Models.Calendar.CalendarTypeStrategies;
|
|
using Wino.Core.Domain.Models.Navigation;
|
|
using Wino.Core.ViewModels;
|
|
using Wino.Messaging.Client.Calendar;
|
|
|
|
namespace Wino.Calendar.ViewModels;
|
|
|
|
public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
|
IRecipient<LoadCalendarMessage>,
|
|
IRecipient<CalendarItemDeleted>,
|
|
IRecipient<CalendarSettingsUpdatedMessage>,
|
|
IRecipient<CalendarItemTappedMessage>,
|
|
IRecipient<CalendarItemDoubleTappedMessage>,
|
|
IRecipient<CalendarItemRightTappedMessage>
|
|
{
|
|
#region Quick Event Creation
|
|
|
|
[ObservableProperty]
|
|
private bool _isQuickEventDialogOpen;
|
|
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(SelectedQuickEventAccountCalendarName))]
|
|
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
|
|
private AccountCalendarViewModel _selectedQuickEventAccountCalendar;
|
|
|
|
public string SelectedQuickEventAccountCalendarName
|
|
{
|
|
get
|
|
{
|
|
return SelectedQuickEventAccountCalendar == null ? "Pick a calendar" : SelectedQuickEventAccountCalendar.Name;
|
|
}
|
|
}
|
|
|
|
[ObservableProperty]
|
|
private List<string> _hourSelectionStrings;
|
|
|
|
// To be able to revert the values when the user enters an invalid time.
|
|
private string _previousSelectedStartTimeString;
|
|
private string _previousSelectedEndTimeString;
|
|
|
|
[ObservableProperty]
|
|
private DateTime? _selectedQuickEventDate;
|
|
|
|
[ObservableProperty]
|
|
private bool _isAllDay;
|
|
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
|
|
private string _selectedStartTimeString;
|
|
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
|
|
private string _selectedEndTimeString;
|
|
|
|
[ObservableProperty]
|
|
private string _location;
|
|
|
|
[ObservableProperty]
|
|
[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 bool CanSaveQuickEvent => SelectedQuickEventAccountCalendar != null &&
|
|
!string.IsNullOrWhiteSpace(EventName) &&
|
|
!string.IsNullOrWhiteSpace(SelectedStartTimeString) &&
|
|
!string.IsNullOrWhiteSpace(SelectedEndTimeString) &&
|
|
QuickEventEndTime > QuickEventStartTime;
|
|
|
|
#endregion
|
|
|
|
#region Data Initialization
|
|
|
|
[ObservableProperty]
|
|
private CalendarOrientation _calendarOrientation = CalendarOrientation.Horizontal;
|
|
|
|
[ObservableProperty]
|
|
private DayRangeCollection _dayRanges = [];
|
|
|
|
[ObservableProperty]
|
|
private int _selectedDateRangeIndex;
|
|
|
|
[ObservableProperty]
|
|
private DayRangeRenderModel _selectedDayRange;
|
|
|
|
[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;
|
|
|
|
private readonly ICalendarService _calendarService;
|
|
private readonly INavigationService _navigationService;
|
|
private readonly IKeyPressService _keyPressService;
|
|
private readonly ICalendarServiceEx _calendarServiceEx;
|
|
private readonly IPreferencesService _preferencesService;
|
|
|
|
// Store latest rendered options.
|
|
private CalendarDisplayType _currentDisplayType;
|
|
private int _displayDayCount;
|
|
|
|
private SemaphoreSlim _calendarLoadingSemaphore = new(1);
|
|
private bool isLoadMoreBlocked = false;
|
|
|
|
[ObservableProperty]
|
|
private CalendarSettings _currentSettings;
|
|
|
|
public IStatePersistanceService StatePersistanceService { get; }
|
|
public IAccountCalendarStateService AccountCalendarStateService { get; }
|
|
|
|
public CalendarPageViewModel(IStatePersistanceService statePersistanceService,
|
|
ICalendarService calendarService,
|
|
INavigationService navigationService,
|
|
IKeyPressService keyPressService,
|
|
ICalendarServiceEx calendarServiceEx,
|
|
IAccountCalendarStateService accountCalendarStateService,
|
|
IPreferencesService preferencesService)
|
|
{
|
|
StatePersistanceService = statePersistanceService;
|
|
AccountCalendarStateService = accountCalendarStateService;
|
|
|
|
_calendarService = calendarService;
|
|
_navigationService = navigationService;
|
|
_keyPressService = keyPressService;
|
|
_calendarServiceEx = calendarServiceEx;
|
|
_preferencesService = preferencesService;
|
|
|
|
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
|
|
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
|
|
}
|
|
|
|
private void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
|
|
=> FilterActiveCalendars(DayRanges);
|
|
|
|
private void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
|
|
=> FilterActiveCalendars(DayRanges);
|
|
|
|
private async void FilterActiveCalendars(IEnumerable<DayRangeRenderModel> dayRangeRenderModels)
|
|
{
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
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.
|
|
// Should be a field ideally.
|
|
private BaseCalendarTypeDrawingStrategy GetDrawingStrategy(CalendarDisplayType displayType)
|
|
{
|
|
return displayType switch
|
|
{
|
|
CalendarDisplayType.Day => new DayCalendarDrawingStrategy(CurrentSettings),
|
|
CalendarDisplayType.Week => new WeekCalendarDrawingStrategy(CurrentSettings),
|
|
CalendarDisplayType.Month => new MonthCalendarDrawingStrategy(CurrentSettings),
|
|
_ => throw new NotImplementedException(),
|
|
};
|
|
}
|
|
|
|
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
|
{
|
|
// Do not call base method because that will unregister messenger recipient.
|
|
// This is a singleton view model and should not be unregistered.
|
|
}
|
|
|
|
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
|
{
|
|
base.OnNavigatedTo(mode, parameters);
|
|
|
|
if (mode == NavigationMode.Back) return;
|
|
|
|
RefreshSettings();
|
|
|
|
// Automatically select the first primary calendar for quick event dialog.
|
|
SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void NavigateSeries()
|
|
{
|
|
if (DisplayDetailsCalendarItemViewModel == null) return;
|
|
|
|
NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Series);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void NavigateEventDetails()
|
|
{
|
|
if (DisplayDetailsCalendarItemViewModel == null) return;
|
|
|
|
NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Single);
|
|
}
|
|
|
|
private void NavigateEvent(CalendarItemViewModel calendarItemViewModel, CalendarEventTargetType calendarEventTargetType)
|
|
{
|
|
var target = new CalendarItemTarget(calendarItemViewModel.CalendarItem, calendarEventTargetType);
|
|
_navigationService.Navigate(WinoPage.EventDetailsPage, target);
|
|
}
|
|
|
|
[RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(CanSaveQuickEvent))]
|
|
private async Task SaveQuickEventAsync()
|
|
{
|
|
// TODO: Create the request with the synchronizer.
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void MoreDetails()
|
|
{
|
|
// TODO: Navigate to advanced event creation page with existing parameters.
|
|
}
|
|
|
|
public void SelectQuickEventTimeRange(TimeSpan startTime, TimeSpan endTime)
|
|
{
|
|
IsAllDay = false;
|
|
|
|
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();
|
|
|
|
// Populate the hour selection strings.
|
|
var timeStrings = new List<string>();
|
|
|
|
for (int hour = 0; hour < 24; hour++)
|
|
{
|
|
for (int minute = 0; minute < 60; minute += 30)
|
|
{
|
|
var time = new DateTime(1, 1, 1, hour, minute, 0);
|
|
|
|
if (CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour)
|
|
{
|
|
timeStrings.Add(time.ToString("HH:mm"));
|
|
}
|
|
else
|
|
{
|
|
timeStrings.Add(time.ToString("h:mm tt"));
|
|
}
|
|
}
|
|
}
|
|
|
|
HourSelectionStrings = timeStrings;
|
|
}
|
|
|
|
partial void OnIsCalendarEnabledChanging(bool oldValue, bool newValue) => Messenger.Send(new CalendarEnableStatusChangedMessage(newValue));
|
|
|
|
private bool ShouldResetDayRanges(LoadCalendarMessage message)
|
|
{
|
|
if (message.ForceRedraw) return true;
|
|
|
|
// Never reset if the initiative is from the app.
|
|
if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false;
|
|
|
|
// 1. Display type is different.
|
|
// 2. Day display count is different.
|
|
// 3. Display date is not in the visible range.
|
|
|
|
if (DayRanges.DisplayRange == null) return false;
|
|
|
|
return
|
|
(_currentDisplayType != StatePersistanceService.CalendarDisplayType ||
|
|
_displayDayCount != StatePersistanceService.DayDisplayCount ||
|
|
!(message.DisplayDate >= DayRanges.DisplayRange.StartDate && message.DisplayDate <= DayRanges.DisplayRange.EndDate));
|
|
}
|
|
|
|
private void AdjustCalendarOrientation()
|
|
{
|
|
// Orientation only changes when we should reset.
|
|
// Handle the FlipView orientation here.
|
|
// We don't want to change the orientation while the item manipulation is going on.
|
|
// That causes a glitch in the UI.
|
|
|
|
bool isRequestedVerticalCalendar = StatePersistanceService.CalendarDisplayType == CalendarDisplayType.Month;
|
|
bool isLastRenderedVerticalCalendar = _currentDisplayType == CalendarDisplayType.Month;
|
|
|
|
if (isRequestedVerticalCalendar && !isLastRenderedVerticalCalendar)
|
|
{
|
|
CalendarOrientation = CalendarOrientation.Vertical;
|
|
}
|
|
else
|
|
{
|
|
CalendarOrientation = CalendarOrientation.Horizontal;
|
|
}
|
|
}
|
|
|
|
public async void Receive(LoadCalendarMessage message)
|
|
{
|
|
await _calendarLoadingSemaphore.WaitAsync();
|
|
|
|
try
|
|
{
|
|
await ExecuteUIThread(() => IsCalendarEnabled = false);
|
|
|
|
if (ShouldResetDayRanges(message))
|
|
{
|
|
Debug.WriteLine("Will reset day ranges.");
|
|
await ClearDayRangeModelsAsync();
|
|
}
|
|
else if (ShouldScrollToItem(message))
|
|
{
|
|
// Scroll to the selected date.
|
|
Messenger.Send(new ScrollToDateMessage(message.DisplayDate));
|
|
Debug.WriteLine("Scrolling to selected date.");
|
|
return;
|
|
}
|
|
|
|
AdjustCalendarOrientation();
|
|
|
|
// This will replace the whole collection because the user initiated a new render.
|
|
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)
|
|
{
|
|
Log.Error(ex, "Error while loading calendar.");
|
|
Debugger.Break();
|
|
}
|
|
finally
|
|
{
|
|
_calendarLoadingSemaphore.Release();
|
|
|
|
await ExecuteUIThread(() => IsCalendarEnabled = true);
|
|
}
|
|
}
|
|
|
|
|
|
private async Task AddDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel)
|
|
{
|
|
if (dayRangeRenderModel == null) return;
|
|
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
DayRanges.Add(dayRangeRenderModel);
|
|
});
|
|
}
|
|
|
|
private async Task InsertDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel, int index)
|
|
{
|
|
if (dayRangeRenderModel == null) return;
|
|
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
DayRanges.Insert(index, dayRangeRenderModel);
|
|
});
|
|
}
|
|
|
|
private async Task RemoveDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel)
|
|
{
|
|
if (dayRangeRenderModel == null) return;
|
|
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
DayRanges.Remove(dayRangeRenderModel);
|
|
});
|
|
}
|
|
|
|
private async Task ClearDayRangeModelsAsync()
|
|
{
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
DayRanges.Clear();
|
|
});
|
|
}
|
|
|
|
private async Task RenderDatesAsync(CalendarInitInitiative calendarInitInitiative,
|
|
DateTime? loadingDisplayDate = null,
|
|
CalendarLoadDirection calendarLoadDirection = CalendarLoadDirection.Replace)
|
|
{
|
|
isLoadMoreBlocked = calendarLoadDirection == CalendarLoadDirection.Replace;
|
|
|
|
// This is the part we arrange the flip view calendar logic.
|
|
|
|
/* Loading for a month of the selected date is fine.
|
|
* If the selected date is in the loaded range, we'll just change the selected flip index to scroll.
|
|
* If the selected date is not in the loaded range:
|
|
* 1. Detect the direction of the scroll.
|
|
* 2. Load the next month.
|
|
* 3. Replace existing month with the new month.
|
|
*/
|
|
|
|
// 2 things are important: How many items should 1 flip have, and, where we should start loading?
|
|
|
|
// User initiated renders must always have a date to start with.
|
|
if (calendarInitInitiative == CalendarInitInitiative.User) Guard.IsNotNull(loadingDisplayDate, nameof(loadingDisplayDate));
|
|
|
|
var strategy = GetDrawingStrategy(StatePersistanceService.CalendarDisplayType);
|
|
var displayDate = loadingDisplayDate.GetValueOrDefault();
|
|
|
|
// How many days should be placed in 1 flip view item?
|
|
int eachFlipItemCount = strategy.GetRenderDayCount(displayDate, StatePersistanceService.DayDisplayCount);
|
|
|
|
DateRange flipLoadRange = null;
|
|
|
|
|
|
if (calendarInitInitiative == CalendarInitInitiative.User || DayRanges.DisplayRange == null)
|
|
{
|
|
flipLoadRange = strategy.GetRenderDateRange(displayDate, StatePersistanceService.DayDisplayCount);
|
|
}
|
|
else
|
|
{
|
|
// App is trying to load.
|
|
// This should be based on direction. We'll load the next or previous range.
|
|
// DisplayDate is either the start or end date of the current visible range.
|
|
|
|
if (calendarLoadDirection == CalendarLoadDirection.Previous)
|
|
{
|
|
flipLoadRange = strategy.GetPreviousDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount);
|
|
}
|
|
else
|
|
{
|
|
flipLoadRange = strategy.GetNextDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount);
|
|
}
|
|
}
|
|
|
|
// Create day ranges for each flip item until we reach the total days to load.
|
|
int totalFlipItemCount = (int)Math.Ceiling((double)flipLoadRange.TotalDays / eachFlipItemCount);
|
|
|
|
List<DayRangeRenderModel> renderModels = new();
|
|
|
|
for (int i = 0; i < totalFlipItemCount; i++)
|
|
{
|
|
var startDate = flipLoadRange.StartDate.AddDays(i * eachFlipItemCount);
|
|
var endDate = startDate.AddDays(eachFlipItemCount);
|
|
|
|
var range = new DateRange(startDate, endDate);
|
|
var renderOptions = new CalendarRenderOptions(range, CurrentSettings);
|
|
|
|
var dayRangeHeaderModel = new DayRangeRenderModel(renderOptions);
|
|
renderModels.Add(dayRangeHeaderModel);
|
|
}
|
|
|
|
// Dates are loaded. Now load the events for them.
|
|
foreach (var renderModel in renderModels)
|
|
{
|
|
await InitializeCalendarEventsForDayRangeAsync(renderModel).ConfigureAwait(false);
|
|
}
|
|
|
|
// Filter by active calendars. This is a quick operation, and things are not on the UI yet.
|
|
FilterActiveCalendars(renderModels);
|
|
|
|
CalendarLoadDirection animationDirection = calendarLoadDirection;
|
|
|
|
//bool removeCurrent = calendarLoadDirection == CalendarLoadDirection.Replace;
|
|
|
|
if (calendarLoadDirection == CalendarLoadDirection.Replace)
|
|
{
|
|
// New date ranges are being replaced.
|
|
// We must preserve existing selection if any, add the items before/after the current one, remove the current one.
|
|
// This will make sure the new dates are animated in the correct direction.
|
|
|
|
isLoadMoreBlocked = true;
|
|
|
|
// Remove all other dates except this one.
|
|
var rangesToRemove = DayRanges.Where(a => a != SelectedDayRange).ToList();
|
|
|
|
foreach (var range in rangesToRemove)
|
|
{
|
|
await RemoveDayRangeModelAsync(range);
|
|
}
|
|
|
|
animationDirection = displayDate <= SelectedDayRange?.CalendarRenderOptions.DateRange.StartDate ?
|
|
CalendarLoadDirection.Previous : CalendarLoadDirection.Next;
|
|
}
|
|
|
|
if (animationDirection == CalendarLoadDirection.Next)
|
|
{
|
|
foreach (var item in renderModels)
|
|
{
|
|
await AddDayRangeModelAsync(item);
|
|
}
|
|
}
|
|
else if (animationDirection == CalendarLoadDirection.Previous)
|
|
{
|
|
// Wait for the animation to finish.
|
|
// Otherwise it somehow shutters a little, which is annoying.
|
|
|
|
// if (!removeCurrent) await Task.Delay(350);
|
|
|
|
// Insert each render model in reverse order.
|
|
for (int i = renderModels.Count - 1; i >= 0; i--)
|
|
{
|
|
await InsertDayRangeModelAsync(renderModels[i], 0);
|
|
}
|
|
}
|
|
|
|
Debug.WriteLine($"Flip count: ({DayRanges.Count})");
|
|
|
|
foreach (var item in DayRanges)
|
|
{
|
|
Debug.WriteLine($"- {item.CalendarRenderOptions.DateRange.ToString()}");
|
|
}
|
|
|
|
//if (removeCurrent)
|
|
//{
|
|
// await RemoveDayRangeModelAsync(SelectedDayRange);
|
|
//}
|
|
|
|
// TODO...
|
|
// await TryConsolidateItemsAsync();
|
|
|
|
isLoadMoreBlocked = false;
|
|
|
|
// Only scroll if the render is initiated by user.
|
|
// Otherwise we'll scroll to the app rendered invisible date range.
|
|
if (calendarInitInitiative == CalendarInitInitiative.User)
|
|
{
|
|
// Save the current settings for the page for later comparison.
|
|
_currentDisplayType = StatePersistanceService.CalendarDisplayType;
|
|
_displayDayCount = StatePersistanceService.DayDisplayCount;
|
|
|
|
Messenger.Send(new ScrollToDateMessage(displayDate));
|
|
}
|
|
}
|
|
|
|
protected override async void OnCalendarItemAdded(CalendarItem calendarItem)
|
|
{
|
|
base.OnCalendarItemAdded(calendarItem);
|
|
|
|
// Check if event falls into the current date range.
|
|
|
|
|
|
if (DayRanges.DisplayRange == null) return;
|
|
|
|
// Check whether this event falls into any of the loaded date ranges.
|
|
var allDaysForEvent = DayRanges.SelectMany(a => a.CalendarDays).Where(a => a.Period.OverlapsWith(calendarItem.Period));
|
|
|
|
foreach (var calendarDay in allDaysForEvent)
|
|
{
|
|
var calendarItemViewModel = new CalendarItemViewModel(calendarItem);
|
|
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel);
|
|
});
|
|
}
|
|
|
|
FilterActiveCalendars(DayRanges);
|
|
}
|
|
|
|
private async Task InitializeCalendarEventsForDayRangeAsync(DayRangeRenderModel dayRangeRenderModel)
|
|
{
|
|
// Clear all events first for all days.
|
|
foreach (var day in dayRangeRenderModel.CalendarDays)
|
|
{
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
day.EventsCollection.Clear();
|
|
});
|
|
}
|
|
|
|
// Initialization is done for all calendars, regardless whether they are actively selected or not.
|
|
// This is because the filtering is cached internally of the calendar items in CalendarEventCollection.
|
|
var allCalendars = AccountCalendarStateService.GroupedAccountCalendars.SelectMany(a => a.AccountCalendars);
|
|
|
|
foreach (var calendarViewModel in allCalendars)
|
|
{
|
|
// Check all the events for the given date range and calendar.
|
|
// Then find the day representation for all the events returned, and add to the collection.
|
|
|
|
var events = await _calendarServiceEx.GetExpandedEventsInDateRangeWithExceptionsAsync(dayRangeRenderModel.Period.Start, dayRangeRenderModel.Period.End, calendarViewModel.AccountCalendar).ConfigureAwait(false);
|
|
|
|
foreach (var @event in events)
|
|
{
|
|
// TODO: Do it in the service.
|
|
@event.AssignedCalendar = calendarViewModel.AccountCalendar;
|
|
|
|
// Find the days that the event falls into.
|
|
var allDaysForEvent = dayRangeRenderModel.CalendarDays.Where(a => a.Period.OverlapsWith(@event.Period));
|
|
|
|
foreach (var calendarDay in allDaysForEvent)
|
|
{
|
|
var calendarItemViewModel = new CalendarItemViewModel(@event);
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task TryConsolidateItemsAsync()
|
|
{
|
|
// Check if trimming is necessary
|
|
if (DayRanges.Count > maxDayRangeSize)
|
|
{
|
|
Debug.WriteLine("Trimming items.");
|
|
|
|
isLoadMoreBlocked = true;
|
|
|
|
var removeCount = DayRanges.Count - maxDayRangeSize;
|
|
|
|
await Task.Delay(500);
|
|
|
|
// Right shifted, remove from the start.
|
|
if (SelectedDateRangeIndex > DayRanges.Count / 2)
|
|
{
|
|
DayRanges.RemoveRange(DayRanges.Take(removeCount).ToList());
|
|
}
|
|
else
|
|
{
|
|
// Left shifted, remove from the end.
|
|
DayRanges.RemoveRange(DayRanges.Skip(DayRanges.Count - removeCount).Take(removeCount));
|
|
}
|
|
|
|
SelectedDateRangeIndex = DayRanges.IndexOf(SelectedDayRange);
|
|
}
|
|
}
|
|
|
|
private bool ShouldScrollToItem(LoadCalendarMessage message)
|
|
{
|
|
// Never scroll if the initiative is from the app.
|
|
if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false;
|
|
|
|
// Nothing to scroll.
|
|
if (DayRanges.Count == 0) return false;
|
|
|
|
if (DayRanges.DisplayRange == null) return false;
|
|
|
|
var selectedDate = message.DisplayDate;
|
|
|
|
return selectedDate >= DayRanges.DisplayRange.StartDate && selectedDate <= DayRanges.DisplayRange.EndDate;
|
|
}
|
|
|
|
partial void OnIsAllDayChanged(bool value)
|
|
{
|
|
if (value)
|
|
{
|
|
SelectedStartTimeString = HourSelectionStrings.FirstOrDefault();
|
|
SelectedEndTimeString = HourSelectionStrings.FirstOrDefault();
|
|
}
|
|
else
|
|
{
|
|
SelectedStartTimeString = _previousSelectedStartTimeString;
|
|
SelectedEndTimeString = _previousSelectedEndTimeString;
|
|
}
|
|
}
|
|
|
|
partial void OnSelectedStartTimeStringChanged(string newValue)
|
|
{
|
|
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
|
|
|
|
if (parsedTime == null)
|
|
{
|
|
SelectedStartTimeString = _previousSelectedStartTimeString;
|
|
}
|
|
else if (IsAllDay)
|
|
{
|
|
_previousSelectedStartTimeString = newValue;
|
|
}
|
|
}
|
|
|
|
partial void OnSelectedEndTimeStringChanged(string newValue)
|
|
{
|
|
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
|
|
|
|
if (parsedTime == null)
|
|
{
|
|
SelectedEndTimeString = _previousSelectedStartTimeString;
|
|
}
|
|
else if (IsAllDay)
|
|
{
|
|
_previousSelectedEndTimeString = newValue;
|
|
}
|
|
}
|
|
|
|
partial void OnSelectedDayRangeChanged(DayRangeRenderModel value)
|
|
{
|
|
DisplayDetailsCalendarItemViewModel = null;
|
|
|
|
if (DayRanges.Count == 0 || SelectedDateRangeIndex < 0) return;
|
|
|
|
var selectedRange = DayRanges[SelectedDateRangeIndex];
|
|
|
|
Messenger.Send(new VisibleDateRangeChangedMessage(new DateRange(selectedRange.Period.Start, selectedRange.Period.End)));
|
|
|
|
if (isLoadMoreBlocked) return;
|
|
|
|
_ = LoadMoreAsync();
|
|
}
|
|
|
|
private async Task LoadMoreAsync()
|
|
{
|
|
try
|
|
{
|
|
await _calendarLoadingSemaphore.WaitAsync();
|
|
|
|
// Depending on the selected index, we'll load more dates.
|
|
// Day ranges may change while the async update is in progress.
|
|
// Therefore we wait for semaphore to be released before we continue.
|
|
// There is no need to load more if the current index is not in ideal position.
|
|
|
|
if (SelectedDateRangeIndex == 0)
|
|
{
|
|
await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Previous);
|
|
}
|
|
else if (SelectedDateRangeIndex == DayRanges.Count - 1)
|
|
{
|
|
await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Next);
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
Debugger.Break();
|
|
}
|
|
finally
|
|
{
|
|
_calendarLoadingSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
public void Receive(CalendarSettingsUpdatedMessage message)
|
|
{
|
|
RefreshSettings();
|
|
|
|
// TODO: This might need throttling due to slider in the settings page for hour height.
|
|
// or make sure the slider does not update on each tick but on focus lost.
|
|
|
|
// Messenger.Send(new LoadCalendarMessage(DateTime.UtcNow.Date, CalendarInitInitiative.App, true));
|
|
}
|
|
|
|
private IEnumerable<CalendarItemViewModel> GetCalendarItems(CalendarItemViewModel calendarItemViewModel, CalendarDayModel selectedDay)
|
|
{
|
|
// 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.
|
|
|
|
if (calendarItemViewModel == null) return Enumerable.Empty<CalendarItemViewModel>();
|
|
|
|
// If the calendar item is not recurring, we can just return it.
|
|
|
|
if (calendarItemViewModel.ItemType == CalendarItemType.Timed || calendarItemViewModel.ItemType == CalendarItemType.RecurringException) return new[] { calendarItemViewModel };
|
|
|
|
return DayRanges
|
|
.SelectMany(a => a.CalendarDays)
|
|
.Select(b => b.EventsCollection.GetCalendarItem(calendarItemViewModel.Id))
|
|
.Where(c => c != null)
|
|
.Cast<CalendarItemViewModel>()
|
|
.Distinct();
|
|
}
|
|
|
|
private void UnselectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
|
|
{
|
|
if (calendarItemViewModel == null) return;
|
|
|
|
var itemsToUnselect = GetCalendarItems(calendarItemViewModel, calendarDay);
|
|
|
|
if (itemsToUnselect == null) return;
|
|
|
|
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);
|
|
|
|
if (itemsToSelect == null) return;
|
|
|
|
foreach (var item in itemsToSelect)
|
|
{
|
|
item.IsSelected = true;
|
|
}
|
|
}
|
|
|
|
public void Receive(CalendarItemTappedMessage message)
|
|
{
|
|
if (message.CalendarItemViewModel == null) return;
|
|
|
|
DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel;
|
|
}
|
|
|
|
public void Receive(CalendarItemDoubleTappedMessage message) => NavigateEvent(message.CalendarItemViewModel, CalendarEventTargetType.Single);
|
|
|
|
public void Receive(CalendarItemRightTappedMessage message)
|
|
{
|
|
|
|
}
|
|
|
|
public async void Receive(CalendarItemDeleted message)
|
|
{
|
|
// Each deleted recurrence will report for it's own.
|
|
|
|
await ExecuteUIThread(() =>
|
|
{
|
|
var deletedItem = message.CalendarItem;
|
|
|
|
// Event might be spreaded into multiple days.
|
|
// Remove from all.
|
|
|
|
// var calendarItems = GetCalendarItems(deletedItem.Id);
|
|
});
|
|
}
|
|
}
|