Files
Wino-Mail/Wino.Calendar.ViewModels/CalendarPageViewModel.cs

871 lines
30 KiB
C#
Raw Normal View History

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;
2025-01-01 17:28:29 +01:00
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;
2024-11-26 20:03:10 +01:00
using Wino.Core.Domain.Models.Calendar.CalendarTypeStrategies;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
2025-05-18 14:06:25 +02:00
namespace Wino.Calendar.ViewModels;
public partial class CalendarPageViewModel : CalendarBaseViewModel,
IRecipient<LoadCalendarMessage>,
IRecipient<CalendarItemDeleted>,
IRecipient<CalendarSettingsUpdatedMessage>,
IRecipient<CalendarItemTappedMessage>,
IRecipient<CalendarItemDoubleTappedMessage>,
IRecipient<CalendarItemRightTappedMessage>
{
2025-05-18 14:06:25 +02:00
#region Quick Event Creation
2025-05-18 14:06:25 +02:00
[ObservableProperty]
private bool _isQuickEventDialogOpen;
2025-05-18 14:06:25 +02:00
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SelectedQuickEventAccountCalendarName))]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
private AccountCalendarViewModel _selectedQuickEventAccountCalendar;
2025-05-18 14:06:25 +02:00
public string SelectedQuickEventAccountCalendarName
{
get
{
2025-05-18 14:06:25 +02:00
return SelectedQuickEventAccountCalendar == null ? "Pick a calendar" : SelectedQuickEventAccountCalendar.Name;
}
2025-05-18 14:06:25 +02:00
}
2025-05-18 14:06:25 +02:00
[ObservableProperty]
private List<string> _hourSelectionStrings;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
// To be able to revert the values when the user enters an invalid time.
private string _previousSelectedStartTimeString;
private string _previousSelectedEndTimeString;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
private DateTime? _selectedQuickEventDate;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
private bool _isAllDay;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
private string _selectedStartTimeString;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
private string _selectedEndTimeString;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
private string _location;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
private string _eventName;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
public DateTime QuickEventStartTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedStartTimeString).Value);
public DateTime QuickEventEndTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedEndTimeString).Value);
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
public bool CanSaveQuickEvent => SelectedQuickEventAccountCalendar != null &&
!string.IsNullOrWhiteSpace(EventName) &&
!string.IsNullOrWhiteSpace(SelectedStartTimeString) &&
!string.IsNullOrWhiteSpace(SelectedEndTimeString) &&
QuickEventEndTime > QuickEventStartTime;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
#endregion
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
#region Data Initialization
2025-05-18 14:06:25 +02:00
[ObservableProperty]
private CalendarOrientation _calendarOrientation = CalendarOrientation.Horizontal;
2025-01-06 21:56:33 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
private DayRangeCollection _dayRanges = [];
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
private int _selectedDateRangeIndex;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
private DayRangeRenderModel _selectedDayRange;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
private bool _isCalendarEnabled = true;
2025-05-18 14:06:25 +02:00
#endregion
2025-05-18 14:06:25 +02:00
#region Event Details
2025-05-18 14:06:25 +02:00
public event EventHandler DetailsShowCalendarItemChanged;
2025-05-18 14:06:25 +02:00
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsEventDetailsVisible))]
private CalendarItemViewModel _displayDetailsCalendarItemViewModel;
2025-05-18 14:06:25 +02:00
public bool IsEventDetailsVisible => DisplayDetailsCalendarItemViewModel != null;
2025-05-18 14:06:25 +02:00
#endregion
2025-05-18 14:06:25 +02:00
// TODO: Get rid of some of the items if we have too many.
private const int maxDayRangeSize = 10;
2025-05-18 14:06:25 +02:00
private readonly ICalendarService _calendarService;
private readonly INavigationService _navigationService;
private readonly IKeyPressService _keyPressService;
private readonly IPreferencesService _preferencesService;
2025-05-18 14:06:25 +02:00
// Store latest rendered options.
private CalendarDisplayType _currentDisplayType;
private int _displayDayCount;
2025-05-18 14:06:25 +02:00
private SemaphoreSlim _calendarLoadingSemaphore = new(1);
private bool isLoadMoreBlocked = false;
2025-05-18 14:06:25 +02:00
[ObservableProperty]
private CalendarSettings _currentSettings;
2025-05-18 14:06:25 +02:00
public IStatePersistanceService StatePersistanceService { get; }
public IAccountCalendarStateService AccountCalendarStateService { get; }
2025-05-18 14:06:25 +02:00
public CalendarPageViewModel(IStatePersistanceService statePersistanceService,
ICalendarService calendarService,
INavigationService navigationService,
IKeyPressService keyPressService,
IAccountCalendarStateService accountCalendarStateService,
IPreferencesService preferencesService)
{
StatePersistanceService = statePersistanceService;
AccountCalendarStateService = accountCalendarStateService;
2025-05-18 14:06:25 +02:00
_calendarService = calendarService;
_navigationService = navigationService;
_keyPressService = keyPressService;
_preferencesService = preferencesService;
2025-05-18 14:06:25 +02:00
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
}
2025-05-18 14:06:25 +02:00
private void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
=> FilterActiveCalendars(DayRanges);
2025-05-18 14:06:25 +02:00
private void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
=> FilterActiveCalendars(DayRanges);
2025-05-18 14:06:25 +02:00
private async void FilterActiveCalendars(IEnumerable<DayRangeRenderModel> dayRangeRenderModels)
{
await ExecuteUIThread(() =>
{
2025-05-18 14:06:25 +02:00
var days = dayRangeRenderModels.SelectMany(a => a.CalendarDays);
2025-05-18 14:06:25 +02:00
days.ForEach(a => a.EventsCollection.FilterByCalendars(AccountCalendarStateService.ActiveCalendars.Select(a => a.Id)));
2025-05-18 14:06:25 +02:00
DisplayDetailsCalendarItemViewModel = null;
});
}
2025-05-18 14:06:25 +02:00
// TODO: Replace when calendar settings are updated.
// Should be a field ideally.
private BaseCalendarTypeDrawingStrategy GetDrawingStrategy(CalendarDisplayType displayType)
{
return displayType switch
{
2025-05-18 14:06:25 +02:00
CalendarDisplayType.Day => new DayCalendarDrawingStrategy(CurrentSettings),
CalendarDisplayType.Week => new WeekCalendarDrawingStrategy(CurrentSettings),
CalendarDisplayType.Month => new MonthCalendarDrawingStrategy(CurrentSettings),
_ => throw new NotImplementedException(),
};
}
2025-05-18 14:06:25 +02:00
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.
}
2025-05-18 14:06:25 +02:00
public override void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
2025-05-18 14:06:25 +02:00
if (mode == NavigationMode.Back) return;
2025-01-14 00:53:54 +01:00
2025-05-18 14:06:25 +02:00
RefreshSettings();
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
// Automatically select the first primary calendar for quick event dialog.
SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary);
}
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[RelayCommand]
private void NavigateSeries()
{
if (DisplayDetailsCalendarItemViewModel == null) return;
2025-05-18 14:06:25 +02:00
NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Series);
}
2025-05-18 14:06:25 +02:00
[RelayCommand]
private void NavigateEventDetails()
{
if (DisplayDetailsCalendarItemViewModel == null) return;
2025-05-18 14:06:25 +02:00
NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Single);
}
2025-05-18 14:06:25 +02:00
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()
{
var durationSeconds = (QuickEventEndTime - QuickEventStartTime).TotalSeconds;
2025-05-18 14:06:25 +02:00
var testCalendarItem = new CalendarItem
2025-01-01 17:28:29 +01:00
{
2025-05-18 14:06:25 +02:00
CalendarId = SelectedQuickEventAccountCalendar.Id,
StartDate = QuickEventStartTime,
DurationInSeconds = durationSeconds,
CreatedAt = DateTime.UtcNow,
Description = string.Empty,
Location = Location,
Title = EventName,
Id = Guid.NewGuid()
};
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
IsQuickEventDialogOpen = false;
await _calendarService.CreateNewCalendarItemAsync(testCalendarItem, null);
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
// TODO: Create the request with the synchronizer.
}
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[RelayCommand]
private void MoreDetails()
{
// TODO: Navigate to advanced event creation page with existing parameters.
}
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
public void SelectQuickEventTimeRange(TimeSpan startTime, TimeSpan endTime)
{
IsAllDay = false;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
SelectedStartTimeString = CurrentSettings.GetTimeString(startTime);
SelectedEndTimeString = CurrentSettings.GetTimeString(endTime);
}
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
// 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);
2025-01-01 17:28:29 +01:00
}
2025-05-18 14:06:25 +02:00
if (newValue != null)
{
2025-05-18 14:06:25 +02:00
SelectCalendarItem(newValue);
}
2025-05-18 14:06:25 +02:00
}
2025-05-18 14:06:25 +02:00
// 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);
2025-05-18 14:06:25 +02:00
private void RefreshSettings()
{
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
// Populate the hour selection strings.
var timeStrings = new List<string>();
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
for (int hour = 0; hour < 24; hour++)
{
for (int minute = 0; minute < 60; minute += 30)
2025-01-01 17:28:29 +01:00
{
2025-05-18 14:06:25 +02:00
var time = new DateTime(1, 1, 1, hour, minute, 0);
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
if (CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour)
{
timeStrings.Add(time.ToString("HH:mm"));
}
else
{
timeStrings.Add(time.ToString("h:mm tt"));
2025-01-01 17:28:29 +01:00
}
}
}
2025-05-18 14:06:25 +02:00
HourSelectionStrings = timeStrings;
}
2025-05-18 14:06:25 +02:00
partial void OnIsCalendarEnabledChanging(bool oldValue, bool newValue) => Messenger.Send(new CalendarEnableStatusChangedMessage(newValue));
2025-05-18 14:06:25 +02:00
private bool ShouldResetDayRanges(LoadCalendarMessage message)
{
if (message.ForceRedraw) return true;
2025-05-18 14:06:25 +02:00
// Never reset if the initiative is from the app.
if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false;
2025-05-18 14:06:25 +02:00
// 1. Display type is different.
// 2. Day display count is different.
// 3. Display date is not in the visible range.
2025-05-18 14:06:25 +02:00
if (DayRanges.DisplayRange == null) return false;
2025-05-18 14:06:25 +02:00
return
(_currentDisplayType != StatePersistanceService.CalendarDisplayType ||
_displayDayCount != StatePersistanceService.DayDisplayCount ||
!(message.DisplayDate >= DayRanges.DisplayRange.StartDate && message.DisplayDate <= DayRanges.DisplayRange.EndDate));
}
2025-05-18 14:06:25 +02:00
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.
2025-05-18 14:06:25 +02:00
bool isRequestedVerticalCalendar = StatePersistanceService.CalendarDisplayType == CalendarDisplayType.Month;
bool isLastRenderedVerticalCalendar = _currentDisplayType == CalendarDisplayType.Month;
2025-05-18 14:06:25 +02:00
if (isRequestedVerticalCalendar && !isLastRenderedVerticalCalendar)
{
2025-05-18 14:06:25 +02:00
CalendarOrientation = CalendarOrientation.Vertical;
}
else
{
CalendarOrientation = CalendarOrientation.Horizontal;
}
}
2025-05-18 14:06:25 +02:00
public async void Receive(LoadCalendarMessage message)
{
await _calendarLoadingSemaphore.WaitAsync();
2025-05-18 14:06:25 +02:00
try
{
await ExecuteUIThread(() => IsCalendarEnabled = false);
2025-05-18 14:06:25 +02:00
if (ShouldResetDayRanges(message))
{
2025-05-18 14:06:25 +02:00
Debug.WriteLine("Will reset day ranges.");
await ClearDayRangeModelsAsync();
}
2025-05-18 14:06:25 +02:00
else if (ShouldScrollToItem(message))
{
2025-05-18 14:06:25 +02:00
// Scroll to the selected date.
Messenger.Send(new ScrollToDateMessage(message.DisplayDate));
Debug.WriteLine("Scrolling to selected date.");
return;
}
2025-05-18 14:06:25 +02:00
AdjustCalendarOrientation();
// This will replace the whole collection because the user initiated a new render.
await RenderDatesAsync(message.CalendarInitInitiative,
message.DisplayDate,
CalendarLoadDirection.Replace);
2025-05-18 14:06:25 +02:00
// 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
{
2025-05-18 14:06:25 +02:00
_calendarLoadingSemaphore.Release();
2025-05-18 14:06:25 +02:00
await ExecuteUIThread(() => IsCalendarEnabled = true);
}
2025-05-18 14:06:25 +02:00
}
private async Task AddDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel)
{
if (dayRangeRenderModel == null) return;
2025-05-18 14:06:25 +02:00
await ExecuteUIThread(() =>
{
2025-05-18 14:06:25 +02:00
DayRanges.Add(dayRangeRenderModel);
});
}
2024-12-31 15:32:03 +01:00
2025-05-18 14:06:25 +02:00
private async Task InsertDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel, int index)
{
if (dayRangeRenderModel == null) return;
2025-05-18 14:06:25 +02:00
await ExecuteUIThread(() =>
{
2025-05-18 14:06:25 +02:00
DayRanges.Insert(index, dayRangeRenderModel);
});
}
2024-12-31 15:32:03 +01:00
2025-05-18 14:06:25 +02:00
private async Task RemoveDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel)
{
if (dayRangeRenderModel == null) return;
2025-05-18 14:06:25 +02:00
await ExecuteUIThread(() =>
{
2025-05-18 14:06:25 +02:00
DayRanges.Remove(dayRangeRenderModel);
});
}
2025-05-18 14:06:25 +02:00
private async Task ClearDayRangeModelsAsync()
{
await ExecuteUIThread(() =>
{
2025-05-18 14:06:25 +02:00
DayRanges.Clear();
});
}
2025-05-18 14:06:25 +02:00
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.
2025-05-18 14:06:25 +02:00
/* 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.
*/
2025-05-18 14:06:25 +02:00
// 2 things are important: How many items should 1 flip have, and, where we should start loading?
2025-05-18 14:06:25 +02:00
// User initiated renders must always have a date to start with.
if (calendarInitInitiative == CalendarInitInitiative.User) Guard.IsNotNull(loadingDisplayDate, nameof(loadingDisplayDate));
2025-05-18 14:06:25 +02:00
var strategy = GetDrawingStrategy(StatePersistanceService.CalendarDisplayType);
var displayDate = loadingDisplayDate.GetValueOrDefault();
2025-05-18 14:06:25 +02:00
// How many days should be placed in 1 flip view item?
int eachFlipItemCount = strategy.GetRenderDayCount(displayDate, StatePersistanceService.DayDisplayCount);
2025-05-18 14:06:25 +02:00
DateRange flipLoadRange = null;
2025-05-18 14:06:25 +02:00
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)
{
2025-05-18 14:06:25 +02:00
flipLoadRange = strategy.GetPreviousDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount);
}
else
{
2025-05-18 14:06:25 +02:00
flipLoadRange = strategy.GetNextDateRange(DayRanges.DisplayRange, StatePersistanceService.DayDisplayCount);
}
2025-05-18 14:06:25 +02:00
}
2025-05-18 14:06:25 +02:00
// Create day ranges for each flip item until we reach the total days to load.
int totalFlipItemCount = (int)Math.Ceiling((double)flipLoadRange.TotalDays / eachFlipItemCount);
2025-05-18 14:06:25 +02:00
List<DayRangeRenderModel> renderModels = new();
2025-05-18 14:06:25 +02:00
for (int i = 0; i < totalFlipItemCount; i++)
{
var startDate = flipLoadRange.StartDate.AddDays(i * eachFlipItemCount);
var endDate = startDate.AddDays(eachFlipItemCount);
2025-05-18 14:06:25 +02:00
var range = new DateRange(startDate, endDate);
var renderOptions = new CalendarRenderOptions(range, CurrentSettings);
2025-05-18 14:06:25 +02:00
var dayRangeHeaderModel = new DayRangeRenderModel(renderOptions);
renderModels.Add(dayRangeHeaderModel);
}
2024-12-28 23:17:16 +01:00
2025-05-18 14:06:25 +02:00
// Dates are loaded. Now load the events for them.
foreach (var renderModel in renderModels)
{
await InitializeCalendarEventsForDayRangeAsync(renderModel).ConfigureAwait(false);
}
2025-05-18 14:06:25 +02:00
// Filter by active calendars. This is a quick operation, and things are not on the UI yet.
FilterActiveCalendars(renderModels);
2025-05-18 14:06:25 +02:00
CalendarLoadDirection animationDirection = calendarLoadDirection;
2025-05-18 14:06:25 +02:00
//bool removeCurrent = calendarLoadDirection == CalendarLoadDirection.Replace;
2025-05-18 14:06:25 +02:00
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.
2025-05-18 14:06:25 +02:00
isLoadMoreBlocked = true;
2025-05-18 14:06:25 +02:00
// Remove all other dates except this one.
var rangesToRemove = DayRanges.Where(a => a != SelectedDayRange).ToList();
2025-05-18 14:06:25 +02:00
foreach (var range in rangesToRemove)
{
2025-05-18 14:06:25 +02:00
await RemoveDayRangeModelAsync(range);
}
2025-05-18 14:06:25 +02:00
animationDirection = displayDate <= SelectedDayRange?.CalendarRenderOptions.DateRange.StartDate ?
CalendarLoadDirection.Previous : CalendarLoadDirection.Next;
}
2025-05-18 14:06:25 +02:00
if (animationDirection == CalendarLoadDirection.Next)
{
foreach (var item in renderModels)
{
await AddDayRangeModelAsync(item);
}
2025-05-18 14:06:25 +02:00
}
else if (animationDirection == CalendarLoadDirection.Previous)
{
// Wait for the animation to finish.
// Otherwise it somehow shutters a little, which is annoying.
2025-05-18 14:06:25 +02:00
// if (!removeCurrent) await Task.Delay(350);
2025-05-18 14:06:25 +02:00
// Insert each render model in reverse order.
for (int i = renderModels.Count - 1; i >= 0; i--)
{
2025-05-18 14:06:25 +02:00
await InsertDayRangeModelAsync(renderModels[i], 0);
}
2025-05-18 14:06:25 +02:00
}
2025-05-18 14:06:25 +02:00
Debug.WriteLine($"Flip count: ({DayRanges.Count})");
2025-05-18 14:06:25 +02:00
foreach (var item in DayRanges)
{
Debug.WriteLine($"- {item.CalendarRenderOptions.DateRange.ToString()}");
}
2025-05-18 14:06:25 +02:00
//if (removeCurrent)
//{
// await RemoveDayRangeModelAsync(SelectedDayRange);
//}
2025-05-18 14:06:25 +02:00
// TODO...
// await TryConsolidateItemsAsync();
2025-05-18 14:06:25 +02:00
isLoadMoreBlocked = false;
2025-05-18 14:06:25 +02:00
// Only scroll if the render is initiated by user.
// Otherwise we'll scroll to the app rendered invisible date range.
if (calendarInitInitiative == CalendarInitInitiative.User)
{
2025-05-18 14:06:25 +02:00
// Save the current settings for the page for later comparison.
_currentDisplayType = StatePersistanceService.CalendarDisplayType;
_displayDayCount = StatePersistanceService.DayDisplayCount;
2025-05-18 14:06:25 +02:00
Messenger.Send(new ScrollToDateMessage(displayDate));
}
}
2025-05-18 14:06:25 +02:00
protected override async void OnCalendarItemAdded(CalendarItem calendarItem)
{
base.OnCalendarItemAdded(calendarItem);
2025-05-18 14:06:25 +02:00
// Check if event falls into the current date range.
2025-05-18 14:06:25 +02:00
if (DayRanges.DisplayRange == null) return;
2025-05-18 14:06:25 +02:00
// 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));
2025-05-18 14:06:25 +02:00
foreach (var calendarDay in allDaysForEvent)
{
var calendarItemViewModel = new CalendarItemViewModel(calendarItem);
await ExecuteUIThread(() =>
{
calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel);
});
}
2025-05-18 14:06:25 +02:00
FilterActiveCalendars(DayRanges);
}
private async Task InitializeCalendarEventsForDayRangeAsync(DayRangeRenderModel dayRangeRenderModel)
{
// Clear all events first for all days.
foreach (var day in dayRangeRenderModel.CalendarDays)
{
2025-05-18 14:06:25 +02:00
await ExecuteUIThread(() =>
{
2025-05-18 14:06:25 +02:00
day.EventsCollection.Clear();
});
}
2025-05-18 14:06:25 +02:00
// 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);
2025-05-18 14:06:25 +02:00
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.
2025-05-18 14:06:25 +02:00
var events = await _calendarService.GetCalendarEventsAsync(calendarViewModel, dayRangeRenderModel).ConfigureAwait(false);
2025-05-18 14:06:25 +02:00
foreach (var @event in events)
{
// Find the days that the event falls into.
var allDaysForEvent = dayRangeRenderModel.CalendarDays.Where(a => a.Period.OverlapsWith(@event.Period));
2025-05-18 14:06:25 +02:00
foreach (var calendarDay in allDaysForEvent)
{
var calendarItemViewModel = new CalendarItemViewModel(@event);
await ExecuteUIThread(() =>
{
2025-05-18 14:06:25 +02:00
calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel);
});
}
}
}
2025-05-18 14:06:25 +02:00
}
2025-05-18 14:06:25 +02:00
private async Task TryConsolidateItemsAsync()
{
// Check if trimming is necessary
if (DayRanges.Count > maxDayRangeSize)
{
2025-05-18 14:06:25 +02:00
Debug.WriteLine("Trimming items.");
2025-05-18 14:06:25 +02:00
isLoadMoreBlocked = true;
2025-05-18 14:06:25 +02:00
var removeCount = DayRanges.Count - maxDayRangeSize;
2025-05-18 14:06:25 +02:00
await Task.Delay(500);
2025-05-18 14:06:25 +02:00
// 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));
}
2025-05-18 14:06:25 +02:00
SelectedDateRangeIndex = DayRanges.IndexOf(SelectedDayRange);
}
2025-05-18 14:06:25 +02:00
}
2025-05-18 14:06:25 +02:00
private bool ShouldScrollToItem(LoadCalendarMessage message)
{
// Never scroll if the initiative is from the app.
if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false;
2025-05-18 14:06:25 +02:00
// Nothing to scroll.
if (DayRanges.Count == 0) return false;
2025-05-18 14:06:25 +02:00
if (DayRanges.DisplayRange == null) return false;
2025-05-18 14:06:25 +02:00
var selectedDate = message.DisplayDate;
2025-05-18 14:06:25 +02:00
return selectedDate >= DayRanges.DisplayRange.StartDate && selectedDate <= DayRanges.DisplayRange.EndDate;
}
2025-05-18 14:06:25 +02:00
partial void OnIsAllDayChanged(bool value)
{
if (value)
2025-01-01 17:28:29 +01:00
{
2025-05-18 14:06:25 +02:00
SelectedStartTimeString = HourSelectionStrings.FirstOrDefault();
SelectedEndTimeString = HourSelectionStrings.FirstOrDefault();
2025-01-01 17:28:29 +01:00
}
2025-05-18 14:06:25 +02:00
else
2025-01-01 17:28:29 +01:00
{
2025-05-18 14:06:25 +02:00
SelectedStartTimeString = _previousSelectedStartTimeString;
SelectedEndTimeString = _previousSelectedEndTimeString;
2025-01-01 17:28:29 +01:00
}
2025-05-18 14:06:25 +02:00
}
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
partial void OnSelectedStartTimeStringChanged(string newValue)
{
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
if (parsedTime == null)
{
SelectedStartTimeString = _previousSelectedStartTimeString;
2025-01-01 17:28:29 +01:00
}
2025-05-18 14:06:25 +02:00
else if (IsAllDay)
{
_previousSelectedStartTimeString = newValue;
}
}
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
partial void OnSelectedEndTimeStringChanged(string newValue)
{
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
if (parsedTime == null)
{
2025-05-18 14:06:25 +02:00
SelectedEndTimeString = _previousSelectedStartTimeString;
}
else if (IsAllDay)
{
_previousSelectedEndTimeString = newValue;
}
}
2025-05-18 14:06:25 +02:00
partial void OnSelectedDayRangeChanged(DayRangeRenderModel value)
{
DisplayDetailsCalendarItemViewModel = null;
2025-05-18 14:06:25 +02:00
if (DayRanges.Count == 0 || SelectedDateRangeIndex < 0) return;
2025-05-18 14:06:25 +02:00
var selectedRange = DayRanges[SelectedDateRangeIndex];
2025-05-18 14:06:25 +02:00
Messenger.Send(new VisibleDateRangeChangedMessage(new DateRange(selectedRange.Period.Start, selectedRange.Period.End)));
2025-05-18 14:06:25 +02:00
if (isLoadMoreBlocked) return;
2025-05-18 14:06:25 +02:00
_ = LoadMoreAsync();
}
private async Task LoadMoreAsync()
{
try
{
2025-05-18 14:06:25 +02:00
await _calendarLoadingSemaphore.WaitAsync();
2025-05-18 14:06:25 +02:00
// 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.
2025-05-18 14:06:25 +02:00
if (SelectedDateRangeIndex == 0)
{
2025-05-18 14:06:25 +02:00
await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Previous);
}
2025-05-18 14:06:25 +02:00
else if (SelectedDateRangeIndex == DayRanges.Count - 1)
{
2025-05-18 14:06:25 +02:00
await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Next);
}
}
2025-05-18 14:06:25 +02:00
catch (Exception)
{
Debugger.Break();
}
finally
{
2025-05-18 14:06:25 +02:00
_calendarLoadingSemaphore.Release();
}
}
2025-05-18 14:06:25 +02:00
public void Receive(CalendarSettingsUpdatedMessage message)
{
RefreshSettings();
2025-05-18 14:06:25 +02:00
// 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.
2025-05-18 14:06:25 +02:00
// Messenger.Send(new LoadCalendarMessage(DateTime.UtcNow.Date, CalendarInitInitiative.App, true));
}
2025-05-18 14:06:25 +02:00
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.
2025-05-18 14:06:25 +02:00
if (!calendarItemViewModel.IsRecurringEvent)
{
2025-05-18 14:06:25 +02:00
return [calendarItemViewModel];
}
else
{
return DayRanges
.SelectMany(a => a.CalendarDays)
.Select(b => b.EventsCollection.GetCalendarItem(calendarItemViewModel.Id))
.Where(c => c != null)
.Cast<CalendarItemViewModel>()
.Distinct();
}
}
2025-05-18 14:06:25 +02:00
private void UnselectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
{
if (calendarItemViewModel == null) return;
2025-05-18 14:06:25 +02:00
var itemsToUnselect = GetCalendarItems(calendarItemViewModel, calendarDay);
2025-05-18 14:06:25 +02:00
foreach (var item in itemsToUnselect)
{
2025-05-18 14:06:25 +02:00
item.IsSelected = false;
}
}
2025-05-18 14:06:25 +02:00
private void SelectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
{
if (calendarItemViewModel == null) return;
2025-05-18 14:06:25 +02:00
var itemsToSelect = GetCalendarItems(calendarItemViewModel, calendarDay);
2025-05-18 14:06:25 +02:00
foreach (var item in itemsToSelect)
{
2025-05-18 14:06:25 +02:00
item.IsSelected = true;
}
2025-05-18 14:06:25 +02:00
}
2025-05-18 14:06:25 +02:00
public void Receive(CalendarItemTappedMessage message)
{
if (message.CalendarItemViewModel == null) return;
2025-05-18 14:06:25 +02:00
DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel;
}
2025-05-18 14:06:25 +02:00
public void Receive(CalendarItemDoubleTappedMessage message) => NavigateEvent(message.CalendarItemViewModel, CalendarEventTargetType.Single);
2025-05-18 14:06:25 +02:00
public void Receive(CalendarItemRightTappedMessage message)
{
2025-05-18 14:06:25 +02:00
}
2025-05-18 14:06:25 +02:00
public async void Receive(CalendarItemDeleted message)
{
// Each deleted recurrence will report for it's own.
2025-05-18 14:06:25 +02:00
await ExecuteUIThread(() =>
{
var deletedItem = message.CalendarItem;
// Event might be spreaded into multiple days.
// Remove from all.
// var calendarItems = GetCalendarItems(deletedItem.Id);
});
}
}