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

858 lines
30 KiB
C#
Raw Normal View History

2026-03-21 00:58:01 +01:00
using System;
2024-11-10 23:28:25 +01:00
using System.Collections.Generic;
using System.Linq;
2026-03-11 19:26:37 +01:00
using System.Runtime.InteropServices;
2024-11-10 23:28:25 +01:00
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
2025-01-01 17:28:29 +01:00
using CommunityToolkit.Mvvm.Input;
2024-11-10 23:28:25 +01:00
using CommunityToolkit.Mvvm.Messaging;
using Itenso.TimePeriod;
using Serilog;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces;
using Wino.Calendar.ViewModels.Messages;
2026-03-08 13:21:42 +01:00
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
2024-11-10 23:28:25 +01:00
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
2026-03-11 19:26:37 +01:00
using Wino.Core.Domain.Models;
2024-11-10 23:28:25 +01:00
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Navigation;
2024-11-10 23:28:25 +01:00
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
using Wino.Messaging.UI;
2024-11-10 23:28:25 +01:00
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>,
2026-03-11 19:26:37 +01:00
IRecipient<AccountRemovedMessage>,
IDisposable
2024-11-10 23:28:25 +01:00
{
2025-05-18 14:06:25 +02:00
#region Quick Event Creation
2024-11-10 23:28:25 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SelectedQuickEventAccountCalendarName))]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
2026-03-16 21:41:22 +01:00
public partial AccountCalendarViewModel SelectedQuickEventAccountCalendar { get; set; }
2024-12-31 22:22:19 +01:00
2025-05-18 14:06:25 +02:00
public string SelectedQuickEventAccountCalendarName
2026-03-21 00:58:01 +01:00
=> SelectedQuickEventAccountCalendar == null ? "Pick a calendar" : SelectedQuickEventAccountCalendar.Name;
2024-12-31 22:22:19 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
2026-03-21 00:58:01 +01:00
public partial List<string> HourSelectionStrings { get; set; } = [];
2025-01-01 17:28:29 +01:00
2026-03-21 00:58:01 +01:00
private string _previousSelectedStartTimeString = string.Empty;
private string _previousSelectedEndTimeString = string.Empty;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
2026-03-16 21:41:22 +01:00
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
public partial DateTime? SelectedQuickEventDate { get; set; }
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
2026-03-16 21:41:22 +01:00
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
public partial bool IsAllDay { get; set; }
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
2026-03-21 00:58:01 +01:00
public partial string SelectedStartTimeString { get; set; } = string.Empty;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
2026-03-21 00:58:01 +01:00
public partial string SelectedEndTimeString { get; set; } = string.Empty;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
2026-03-21 00:58:01 +01:00
public partial string Location { get; set; } = string.Empty;
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))]
2026-03-21 00:58:01 +01:00
public partial string EventName { get; set; } = string.Empty;
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
2026-03-16 21:41:22 +01:00
public bool CanSaveQuickEvent
{
get
{
if (SelectedQuickEventAccountCalendar == null ||
SelectedQuickEventDate == null ||
string.IsNullOrWhiteSpace(EventName) ||
string.IsNullOrWhiteSpace(SelectedStartTimeString) ||
string.IsNullOrWhiteSpace(SelectedEndTimeString))
{
return false;
}
var startTime = CurrentSettings.GetTimeSpan(SelectedStartTimeString);
var endTime = CurrentSettings.GetTimeSpan(SelectedEndTimeString);
if (!startTime.HasValue || !endTime.HasValue)
{
return false;
}
return IsAllDay || endTime > startTime;
}
}
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
2026-03-21 00:58:01 +01:00
#region Visible Range
2025-05-18 14:06:25 +02:00
[ObservableProperty]
2026-03-21 00:58:01 +01:00
public partial VisibleDateRange CurrentVisibleRange { get; set; }
2025-05-18 14:06:25 +02:00
[ObservableProperty]
2026-03-21 00:58:01 +01:00
public partial string VisibleDateRangeText { get; set; } = string.Empty;
2025-05-18 14:06:25 +02:00
[ObservableProperty]
2026-03-21 00:58:01 +01:00
public partial DateRange LoadedDateWindow { get; set; }
2025-05-18 14:06:25 +02:00
[ObservableProperty]
2026-01-06 12:07:22 +01:00
public partial bool IsCalendarEnabled { get; set; } = true;
2024-11-10 23:28:25 +01:00
2026-03-23 10:22:47 +01:00
[ObservableProperty]
public partial IReadOnlyList<CalendarItemViewModel> CalendarItems { get; set; } = [];
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;
2026-01-06 12:07:22 +01:00
public bool CanJoinOnline => DisplayDetailsCalendarItemViewModel != null &&
2026-03-21 00:58:01 +01:00
!string.IsNullOrEmpty(DisplayDetailsCalendarItemViewModel.CalendarItem.HtmlLink);
2026-01-06 12:07:22 +01:00
2025-05-18 14:06:25 +02:00
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsEventDetailsVisible))]
2026-01-06 12:07:22 +01:00
[NotifyCanExecuteChangedFor(nameof(JoinOnlineCommand))]
[NotifyPropertyChangedFor(nameof(CanJoinOnline))]
2026-01-06 12:07:22 +01:00
public partial CalendarItemViewModel DisplayDetailsCalendarItemViewModel { get; set; }
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
private readonly ICalendarService _calendarService;
private readonly INavigationService _navigationService;
2026-01-06 12:07:22 +01:00
private readonly INativeAppService _nativeAppService;
2025-05-18 14:06:25 +02:00
private readonly IPreferencesService _preferencesService;
2025-12-30 11:59:54 +01:00
private readonly IWinoRequestDelegator _winoRequestDelegator;
2026-03-08 13:21:42 +01:00
private readonly IMailDialogService _dialogService;
2026-03-21 00:58:01 +01:00
private readonly IDateContextProvider _dateContextProvider;
private readonly ICalendarRangeTextFormatter _calendarRangeTextFormatter;
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
private readonly SemaphoreSlim _calendarLoadingSemaphore = new(1);
2026-03-11 19:26:37 +01:00
private bool _subscriptionsAttached;
private CancellationTokenSource _pageLifetimeCts = new();
private long _pageLifetimeVersion;
2026-03-21 00:58:01 +01:00
private List<CalendarItemViewModel> _loadedCalendarItems = [];
2025-05-18 14:06:25 +02:00
[ObservableProperty]
2026-03-16 21:41:22 +01:00
public partial CalendarSettings CurrentSettings { get; set; }
2024-11-10 23:28:25 +01:00
2025-05-18 14:06:25 +02:00
public IStatePersistanceService StatePersistanceService { get; }
public IAccountCalendarStateService AccountCalendarStateService { get; }
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
public CalendarPageViewModel(
IStatePersistanceService statePersistanceService,
ICalendarService calendarService,
INavigationService navigationService,
IKeyPressService keyPressService,
INativeAppService nativeAppService,
IAccountCalendarStateService accountCalendarStateService,
IPreferencesService preferencesService,
IWinoRequestDelegator winoRequestDelegator,
IMailDialogService dialogService,
IDateContextProvider dateContextProvider,
ICalendarRangeTextFormatter calendarRangeTextFormatter)
2025-05-18 14:06:25 +02:00
{
StatePersistanceService = statePersistanceService;
AccountCalendarStateService = accountCalendarStateService;
_calendarService = calendarService;
_navigationService = navigationService;
2026-01-06 12:07:22 +01:00
_nativeAppService = nativeAppService;
2025-05-18 14:06:25 +02:00
_preferencesService = preferencesService;
2025-12-30 11:59:54 +01:00
_winoRequestDelegator = winoRequestDelegator;
2026-03-08 13:21:42 +01:00
_dialogService = dialogService;
2026-03-21 00:58:01 +01:00
_dateContextProvider = dateContextProvider;
_calendarRangeTextFormatter = calendarRangeTextFormatter;
2026-03-21 00:58:01 +01:00
RefreshSettings();
2025-05-18 14:06:25 +02:00
}
2024-11-10 23:28:25 +01:00
2026-03-08 13:21:42 +01:00
public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
{
if (args.Handled || args.Mode != WinoApplicationMode.Calendar || args.Action != KeyboardShortcutAction.Delete)
return;
if (DisplayDetailsCalendarItemViewModel?.CalendarItem == null)
return;
if (DisplayDetailsCalendarItemViewModel.CalendarItem.IsRecurringParent)
{
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
Translator.DialogMessage_DeleteRecurringSeriesMessage,
Translator.DialogMessage_DeleteRecurringSeriesTitle,
Translator.Buttons_Delete);
if (!confirmed)
return;
}
var preparationRequest = new CalendarOperationPreparationRequest(
CalendarSynchronizerOperation.DeleteEvent,
DisplayDetailsCalendarItemViewModel.CalendarItem,
null);
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
DisplayDetailsCalendarItemViewModel = null;
args.Handled = true;
}
2025-12-27 19:16:24 +01:00
protected override void RegisterRecipients()
{
base.RegisterRecipients();
Messenger.Unregister<LoadCalendarMessage>(this);
Messenger.Unregister<CalendarSettingsUpdatedMessage>(this);
Messenger.Unregister<CalendarItemTappedMessage>(this);
Messenger.Unregister<CalendarItemDoubleTappedMessage>(this);
Messenger.Unregister<CalendarItemRightTappedMessage>(this);
Messenger.Unregister<AccountRemovedMessage>(this);
2026-02-19 01:37:43 +01:00
Messenger.Register<LoadCalendarMessage>(this);
Messenger.Register<CalendarSettingsUpdatedMessage>(this);
Messenger.Register<CalendarItemTappedMessage>(this);
Messenger.Register<CalendarItemDoubleTappedMessage>(this);
Messenger.Register<CalendarItemRightTappedMessage>(this);
Messenger.Register<AccountRemovedMessage>(this);
2025-12-27 19:16:24 +01:00
}
2026-03-11 19:26:37 +01:00
protected override void UnregisterRecipients()
{
base.UnregisterRecipients();
Messenger.Unregister<LoadCalendarMessage>(this);
Messenger.Unregister<CalendarSettingsUpdatedMessage>(this);
Messenger.Unregister<CalendarItemTappedMessage>(this);
Messenger.Unregister<CalendarItemDoubleTappedMessage>(this);
Messenger.Unregister<CalendarItemRightTappedMessage>(this);
Messenger.Unregister<AccountRemovedMessage>(this);
}
2025-05-18 14:06:25 +02:00
private void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
2026-03-25 15:49:14 +01:00
{
EnsureSelectedQuickEventAccountCalendar();
_ = ReloadCurrentVisibleRangeAsync();
}
2025-05-18 14:06:25 +02:00
private void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
2026-03-25 15:49:14 +01:00
{
EnsureSelectedQuickEventAccountCalendar();
_ = ReloadCurrentVisibleRangeAsync();
}
2026-01-06 12:07:22 +01:00
[RelayCommand(CanExecute = nameof(CanJoinOnline))]
private async Task JoinOnlineAsync()
{
2026-03-21 00:58:01 +01:00
if (DisplayDetailsCalendarItemViewModel == null || string.IsNullOrEmpty(DisplayDetailsCalendarItemViewModel.CalendarItem.HtmlLink))
return;
2026-01-06 12:07:22 +01:00
await _nativeAppService.LaunchUriAsync(new Uri(DisplayDetailsCalendarItemViewModel.CalendarItem.HtmlLink));
}
2025-05-18 14:06:25 +02:00
public override void OnNavigatedTo(NavigationMode mode, object parameters)
{
2026-03-11 19:26:37 +01:00
ResetPageLifetime();
base.OnNavigatedTo(mode, parameters);
AttachSubscriptions();
2025-05-18 14:06:25 +02:00
RefreshSettings();
2026-03-11 19:26:37 +01:00
IsCalendarEnabled = true;
2026-03-25 15:49:14 +01:00
EnsureSelectedQuickEventAccountCalendar();
2025-05-18 14:06:25 +02:00
}
2025-01-01 17:28:29 +01:00
2026-03-08 18:40:43 +01:00
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
{
2026-03-11 19:26:37 +01:00
base.OnNavigatedFrom(mode, parameters);
if (StatePersistanceService.ApplicationMode == WinoApplicationMode.Calendar)
{
CancelPendingOperations();
DetachSubscriptions();
return;
}
CleanupForShellDeactivation();
}
private void AttachSubscriptions()
{
if (_subscriptionsAttached)
return;
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
_subscriptionsAttached = true;
}
private void DetachSubscriptions()
{
if (!_subscriptionsAttached)
return;
AccountCalendarStateService.AccountCalendarSelectionStateChanged -= UpdateAccountCalendarRequested;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged -= AccountCalendarStateCollectivelyChanged;
_subscriptionsAttached = false;
}
private void ReleasePageState()
{
DetachSubscriptions();
DisplayDetailsCalendarItemViewModel = null;
SelectedQuickEventAccountCalendar = null;
SelectedQuickEventDate = null;
HourSelectionStrings = [];
2026-03-21 00:58:01 +01:00
CurrentVisibleRange = null;
VisibleDateRangeText = string.Empty;
LoadedDateWindow = null;
_loadedCalendarItems = [];
2026-03-23 10:22:47 +01:00
CalendarItems = [];
2026-03-11 19:26:37 +01:00
}
public void Dispose()
{
CleanupForShellDeactivation();
}
public void CleanupForShellDeactivation()
{
CancelPendingOperations();
ReleasePageState();
GC.SuppressFinalize(this);
}
2026-03-21 00:58:01 +01:00
public bool RestoreVisibleState() => CurrentVisibleRange != null;
2026-03-11 19:26:37 +01:00
public DateTime GetRestoreDate()
2026-03-21 00:58:01 +01:00
=> CurrentVisibleRange?.AnchorDate.ToDateTime(TimeOnly.MinValue) ?? DateTime.Now.Date;
2026-03-11 19:26:37 +01:00
private long CurrentPageLifetimeVersion => Interlocked.Read(ref _pageLifetimeVersion);
private bool IsPageActive(long lifetimeVersion)
=> lifetimeVersion == CurrentPageLifetimeVersion && !_pageLifetimeCts.IsCancellationRequested;
private void ResetPageLifetime()
{
CancelPendingOperations();
_pageLifetimeCts = new CancellationTokenSource();
Interlocked.Increment(ref _pageLifetimeVersion);
}
private void CancelPendingOperations()
{
if (!_pageLifetimeCts.IsCancellationRequested)
{
_pageLifetimeCts.Cancel();
}
}
private async Task<bool> WaitForCalendarLoadingLockAsync(long lifetimeVersion)
{
if (!IsPageActive(lifetimeVersion))
return false;
try
{
2026-03-21 00:58:01 +01:00
await _calendarLoadingSemaphore.WaitAsync(_pageLifetimeCts.Token).ConfigureAwait(false);
2026-03-11 19:26:37 +01:00
return IsPageActive(lifetimeVersion);
}
catch (OperationCanceledException)
{
return false;
}
catch (ObjectDisposedException)
{
return false;
}
}
private void ReleaseCalendarLoadingLock()
{
try
{
_calendarLoadingSemaphore.Release();
}
catch (ObjectDisposedException)
{
}
catch (SemaphoreFullException)
{
}
}
private async Task ExecuteUIThreadIfActiveAsync(long lifetimeVersion, Action action)
{
if (action == null || !IsPageActive(lifetimeVersion))
return;
try
{
await ExecuteUIThread(() =>
{
if (IsPageActive(lifetimeVersion))
{
action();
}
}).ConfigureAwait(false);
}
catch (COMException) when (!IsPageActive(lifetimeVersion))
{
}
catch (ObjectDisposedException) when (!IsPageActive(lifetimeVersion))
{
}
2026-03-08 18:40:43 +01:00
}
2025-05-18 14:06:25 +02:00
[RelayCommand]
private void NavigateSeries()
{
2026-03-21 00:58:01 +01:00
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()
{
2026-03-21 00:58:01 +01:00
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()
{
2026-03-21 00:58:01 +01:00
var startDate = IsAllDay ? SelectedQuickEventDate.Value.Date : QuickEventStartTime;
var endDate = IsAllDay ? SelectedQuickEventDate.Value.Date.AddDays(1) : QuickEventEndTime;
var composeResult = new CalendarEventComposeResult
2025-12-30 11:59:54 +01:00
{
2026-03-21 00:58:01 +01:00
CalendarId = SelectedQuickEventAccountCalendar.Id,
AccountId = SelectedQuickEventAccountCalendar.Account.Id,
Title = EventName,
Location = Location ?? string.Empty,
HtmlNotes = string.Empty,
StartDate = startDate,
EndDate = endDate,
IsAllDay = IsAllDay,
TimeZoneId = TimeZoneInfo.Local.Id,
ShowAs = SelectedQuickEventAccountCalendar.DefaultShowAs,
SelectedReminders = [],
Attendees = [],
Attachments = [],
Recurrence = string.Empty,
RecurrenceSummary = string.Empty
};
var preparationRequest = new CalendarOperationPreparationRequest(
CalendarSynchronizerOperation.CreateEvent,
ComposeResult: composeResult);
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
2025-05-18 14:06:25 +02:00
}
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
[RelayCommand]
2026-03-21 00:58:01 +01:00
private void GoToEventComposePage()
2025-05-18 14:06:25 +02:00
{
2026-03-06 17:46:38 +01:00
if (SelectedQuickEventDate == null)
return;
2026-03-21 00:58:01 +01:00
var startDate = SelectedQuickEventDate.Value;
var endDate = SelectedQuickEventDate.Value.AddMinutes(30);
2026-03-06 17:46:38 +01:00
if (!IsAllDay)
{
var selectedStartTime = CurrentSettings.GetTimeSpan(SelectedStartTimeString);
var selectedEndTime = CurrentSettings.GetTimeSpan(SelectedEndTimeString);
if (selectedStartTime.HasValue)
{
startDate = SelectedQuickEventDate.Value.Date.Add(selectedStartTime.Value);
}
if (selectedEndTime.HasValue)
{
endDate = SelectedQuickEventDate.Value.Date.Add(selectedEndTime.Value);
}
}
else
{
startDate = SelectedQuickEventDate.Value.Date;
endDate = SelectedQuickEventDate.Value.Date.AddDays(1);
}
_navigationService.Navigate(WinoPage.CalendarEventComposePage, new CalendarEventComposeNavigationArgs
{
SelectedCalendarId = SelectedQuickEventAccountCalendar?.Id,
Title = EventName ?? string.Empty,
Location = Location ?? string.Empty,
IsAllDay = IsAllDay,
StartDate = startDate,
EndDate = endDate
});
2025-05-18 14:06:25 +02:00
}
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;
SelectedStartTimeString = CurrentSettings.GetTimeString(startTime);
SelectedEndTimeString = CurrentSettings.GetTimeString(endTime);
}
2025-01-01 17:28:29 +01:00
2025-05-18 14:06:25 +02:00
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
var timeStrings = new List<string>();
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);
2026-03-21 00:58:01 +01:00
timeStrings.Add(CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour
? time.ToString("HH:mm")
: time.ToString("h:mm tt"));
2025-01-01 17:28:29 +01:00
}
}
2025-05-18 14:06:25 +02:00
HourSelectionStrings = timeStrings;
2026-03-21 00:58:01 +01:00
if (CurrentVisibleRange != null)
2024-11-10 23:28:25 +01:00
{
2026-03-21 00:58:01 +01:00
VisibleDateRangeText = _calendarRangeTextFormatter.Format(CurrentVisibleRange, _dateContextProvider);
2025-05-18 14:06:25 +02:00
}
}
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
public async Task ApplyDisplayRequestAsync(CalendarDisplayRequest request, bool forceReload = false)
2025-05-18 14:06:25 +02:00
{
2026-03-11 19:26:37 +01:00
var lifetimeVersion = CurrentPageLifetimeVersion;
var hasLoadingLock = await WaitForCalendarLoadingLockAsync(lifetimeVersion).ConfigureAwait(false);
if (!hasLoadingLock)
return;
2025-05-18 14:06:25 +02:00
try
{
2026-03-11 19:26:37 +01:00
await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => IsCalendarEnabled = false).ConfigureAwait(false);
2026-03-11 19:26:37 +01:00
if (!IsPageActive(lifetimeVersion))
return;
2026-03-25 13:39:27 +01:00
var currentSettings = CurrentSettings;
if (currentSettings == null)
{
RefreshSettings();
currentSettings = CurrentSettings;
}
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
var today = _dateContextProvider.GetToday();
2026-03-25 13:39:27 +01:00
var visibleRange = CalendarRangeResolver.Resolve(request, currentSettings, today);
var previousRange = CalendarRangeResolver.Navigate(visibleRange, -1, currentSettings, today);
var nextRange = CalendarRangeResolver.Navigate(visibleRange, 1, currentSettings, today);
2026-03-21 00:58:01 +01:00
var loadedDateWindow = new DateRange(
previousRange.StartDate.ToDateTime(TimeOnly.MinValue),
nextRange.EndDate.AddDays(1).ToDateTime(TimeOnly.MinValue));
2025-05-18 14:06:25 +02:00
2026-03-21 00:58:01 +01:00
var shouldReload = forceReload || !IsSameVisibleRange(CurrentVisibleRange, visibleRange) || !IsSameDateRange(LoadedDateWindow, loadedDateWindow);
2026-03-25 13:39:27 +01:00
List<CalendarItemViewModel> loadedItems = null;
2026-03-21 00:58:01 +01:00
if (shouldReload)
2026-03-11 19:26:37 +01:00
{
2026-03-25 13:39:27 +01:00
loadedItems = await LoadCalendarItemsAsync(loadedDateWindow, lifetimeVersion).ConfigureAwait(false);
2026-03-21 00:58:01 +01:00
if (!IsPageActive(lifetimeVersion))
return;
2026-03-25 13:39:27 +01:00
}
2026-03-21 00:58:01 +01:00
2026-03-25 13:39:27 +01:00
await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () =>
{
if (loadedItems != null)
2026-03-21 00:58:01 +01:00
{
_loadedCalendarItems = loadedItems;
2026-03-23 10:22:47 +01:00
CalendarItems = loadedItems;
2026-03-25 13:39:27 +01:00
}
2026-03-21 00:58:01 +01:00
2026-03-25 15:49:14 +01:00
EnsureSelectedQuickEventAccountCalendar();
2026-03-21 00:58:01 +01:00
CurrentVisibleRange = visibleRange;
LoadedDateWindow = loadedDateWindow;
VisibleDateRangeText = _calendarRangeTextFormatter.Format(visibleRange, _dateContextProvider);
if (DisplayDetailsCalendarItemViewModel != null && !IsCalendarActive(DisplayDetailsCalendarItemViewModel.AssignedCalendar?.Id))
{
DisplayDetailsCalendarItemViewModel = null;
}
}).ConfigureAwait(false);
2026-03-11 19:26:37 +01:00
}
catch (OperationCanceledException)
{
}
catch (COMException) when (!IsPageActive(lifetimeVersion))
{
}
catch (ObjectDisposedException) when (!IsPageActive(lifetimeVersion))
{
2025-05-18 14:06:25 +02:00
}
catch (Exception ex)
{
2026-03-21 00:58:01 +01:00
Log.Error(ex, "Error while loading visible calendar range.");
2025-05-18 14:06:25 +02:00
}
finally
{
2026-03-11 19:26:37 +01:00
ReleaseCalendarLoadingLock();
await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => IsCalendarEnabled = true).ConfigureAwait(false);
}
2025-05-18 14:06:25 +02:00
}
2026-03-21 00:58:01 +01:00
public Task ReloadCurrentVisibleRangeAsync()
2025-05-18 14:06:25 +02:00
{
2026-03-21 00:58:01 +01:00
if (CurrentVisibleRange == null)
return Task.CompletedTask;
2026-03-21 00:58:01 +01:00
return ApplyDisplayRequestAsync(new CalendarDisplayRequest(CurrentVisibleRange.DisplayType, CurrentVisibleRange.AnchorDate), forceReload: true);
2025-05-18 14:06:25 +02:00
}
2024-12-31 15:32:03 +01:00
2026-03-25 09:45:49 +01:00
public async Task<IReadOnlyList<CalendarItem>> SearchCalendarItemsAsync(string queryText, int limit, CancellationToken cancellationToken)
{
var results = await _calendarService.SearchCalendarItemsAsync(queryText, limit, cancellationToken).ConfigureAwait(false);
var activeCalendarIds = AccountCalendarStateService.ActiveCalendars.Select(calendar => calendar.Id).ToHashSet();
return results
.Where(result => activeCalendarIds.Contains(result.CalendarId))
.ToList();
}
public void OpenCalendarSearchResult(CalendarItem calendarItem)
{
ArgumentNullException.ThrowIfNull(calendarItem);
NavigateEvent(new CalendarItemViewModel(calendarItem), CalendarEventTargetType.Single);
}
2026-03-21 00:58:01 +01:00
private async Task<List<CalendarItemViewModel>> LoadCalendarItemsAsync(DateRange loadedDateWindow, long lifetimeVersion)
2025-05-18 14:06:25 +02:00
{
2026-03-21 00:58:01 +01:00
var loadedItems = new Dictionary<Guid, CalendarItemViewModel>();
var loadPeriod = new TimeRange(loadedDateWindow.StartDate, loadedDateWindow.EndDate);
2026-03-25 15:49:14 +01:00
foreach (var calendarViewModel in AccountCalendarStateService.ActiveCalendars)
{
2026-03-21 00:58:01 +01:00
if (!IsPageActive(lifetimeVersion))
return [];
2024-12-31 15:32:03 +01:00
2026-03-21 00:58:01 +01:00
var events = await _calendarService.GetCalendarEventsAsync(calendarViewModel, loadPeriod).ConfigureAwait(false);
foreach (var calendarItem in events)
{
if (calendarItem.IsRecurringParent || calendarItem.IsHidden)
continue;
2026-03-21 00:58:01 +01:00
calendarItem.AssignedCalendar ??= calendarViewModel;
2026-03-21 00:58:01 +01:00
if (!loadedItems.ContainsKey(calendarItem.Id))
{
loadedItems.Add(calendarItem.Id, new CalendarItemViewModel(calendarItem));
}
}
}
return loadedItems.Values.ToList();
2025-05-18 14:06:25 +02:00
}
2026-03-21 00:58:01 +01:00
private static bool IsSameVisibleRange(VisibleDateRange current, VisibleDateRange next)
2026-03-11 19:26:37 +01:00
{
2026-03-21 00:58:01 +01:00
if (current == null && next == null)
return true;
2026-03-11 19:26:37 +01:00
2026-03-21 00:58:01 +01:00
if (current == null || next == null)
return false;
2026-03-11 19:26:37 +01:00
2026-03-21 00:58:01 +01:00
return current.DisplayType == next.DisplayType &&
current.AnchorDate == next.AnchorDate &&
current.StartDate == next.StartDate &&
current.EndDate == next.EndDate;
2026-03-11 19:26:37 +01:00
}
2026-03-21 00:58:01 +01:00
private static bool IsSameDateRange(DateRange current, DateRange next)
2025-05-18 14:06:25 +02:00
{
2026-03-21 00:58:01 +01:00
if (current == null && next == null)
return true;
2026-03-11 19:26:37 +01:00
2026-03-21 00:58:01 +01:00
if (current == null || next == null)
return false;
2025-05-18 14:06:25 +02:00
2026-03-21 00:58:01 +01:00
return current.StartDate == next.StartDate && current.EndDate == next.EndDate;
}
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
private bool IsCalendarActive(Guid? calendarId)
=> calendarId.HasValue && AccountCalendarStateService.ActiveCalendars.Any(calendar => calendar.Id == calendarId.Value);
2024-11-10 23:28:25 +01:00
2026-03-25 15:49:14 +01:00
private void EnsureSelectedQuickEventAccountCalendar()
{
if (SelectedQuickEventAccountCalendar != null && IsCalendarActive(SelectedQuickEventAccountCalendar.Id))
{
return;
}
SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary)
?? AccountCalendarStateService.ActiveCalendars.FirstOrDefault();
}
2026-03-21 00:58:01 +01:00
public async void Receive(LoadCalendarMessage message)
=> await ApplyDisplayRequestAsync(message.DisplayRequest, message.ForceReload);
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
public void Receive(CalendarSettingsUpdatedMessage message)
{
RefreshSettings();
_ = ReloadCurrentVisibleRangeAsync();
}
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
public void Receive(CalendarItemTappedMessage message)
{
if (message.CalendarItemViewModel == null)
return;
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel;
}
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
public void Receive(CalendarItemDoubleTappedMessage message)
=> NavigateEvent(message.CalendarItemViewModel, CalendarEventTargetType.Single);
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
public void Receive(CalendarItemRightTappedMessage message)
{
}
2026-03-21 00:58:01 +01:00
public async void Receive(AccountRemovedMessage message)
{
if (DisplayDetailsCalendarItemViewModel?.AssignedCalendar?.AccountId == message.Account.Id)
2025-05-18 14:06:25 +02:00
{
2026-03-21 00:58:01 +01:00
DisplayDetailsCalendarItemViewModel = null;
2025-05-18 14:06:25 +02:00
}
2026-03-25 15:49:14 +01:00
EnsureSelectedQuickEventAccountCalendar();
2026-03-21 00:58:01 +01:00
await ReloadCurrentVisibleRangeAsync().ConfigureAwait(false);
}
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
protected override void OnCalendarItemDeleted(CalendarItem calendarItem)
{
base.OnCalendarItemDeleted(calendarItem);
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
if (DisplayDetailsCalendarItemViewModel?.Id == calendarItem.Id ||
DisplayDetailsCalendarItemViewModel?.CalendarItem?.RecurringCalendarItemId == calendarItem.Id)
2025-05-18 14:06:25 +02:00
{
2026-03-21 00:58:01 +01:00
DisplayDetailsCalendarItemViewModel = null;
}
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
if (ShouldReloadFor(calendarItem))
{
_ = ReloadCurrentVisibleRangeAsync();
2026-03-20 13:26:16 +01:00
}
2026-03-21 00:58:01 +01:00
}
2026-03-20 13:26:16 +01:00
2026-03-21 00:58:01 +01:00
protected override void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source)
{
base.OnCalendarItemUpdated(calendarItem, source);
2026-03-20 13:26:16 +01:00
2026-03-21 00:58:01 +01:00
if (DisplayDetailsCalendarItemViewModel?.Id == calendarItem.Id)
2026-03-20 13:26:16 +01:00
{
2026-03-21 00:58:01 +01:00
calendarItem.AssignedCalendar ??= DisplayDetailsCalendarItemViewModel.AssignedCalendar;
DisplayDetailsCalendarItemViewModel = new CalendarItemViewModel(calendarItem);
2025-05-18 14:06:25 +02:00
}
2024-12-28 23:17:16 +01:00
2026-03-21 00:58:01 +01:00
if (ShouldReloadFor(calendarItem))
2025-05-18 14:06:25 +02:00
{
2026-03-21 00:58:01 +01:00
_ = ReloadCurrentVisibleRangeAsync();
2025-05-18 14:06:25 +02:00
}
2026-03-21 00:58:01 +01:00
}
2026-03-21 00:58:01 +01:00
protected override void OnCalendarItemAdded(CalendarItem calendarItem)
{
base.OnCalendarItemAdded(calendarItem);
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
if (calendarItem.IsRecurringParent)
2025-05-18 14:06:25 +02:00
{
2026-03-21 00:58:01 +01:00
_ = ReloadCurrentVisibleRangeAsync();
2026-03-11 19:26:37 +01:00
return;
2025-05-18 14:06:25 +02:00
}
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
if (ShouldReloadFor(calendarItem))
2025-05-18 14:06:25 +02:00
{
2026-03-21 00:58:01 +01:00
_ = ReloadCurrentVisibleRangeAsync();
2025-05-18 14:06:25 +02:00
}
2026-03-21 00:58:01 +01:00
}
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
private bool ShouldReloadFor(CalendarItem calendarItem)
{
if (calendarItem == null || LoadedDateWindow == null)
return false;
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
var loadedWindow = new TimeRange(LoadedDateWindow.StartDate, LoadedDateWindow.EndDate);
return loadedWindow.OverlapsWith(calendarItem.Period);
}
2024-11-10 23:28:25 +01:00
2026-03-21 00:58:01 +01:00
partial void OnIsAllDayChanged(bool value)
{
if (value)
2025-05-18 14:06:25 +02:00
{
2026-03-21 00:58:01 +01:00
_previousSelectedStartTimeString = SelectedStartTimeString;
_previousSelectedEndTimeString = SelectedEndTimeString;
SelectedStartTimeString = HourSelectionStrings.FirstOrDefault();
SelectedEndTimeString = HourSelectionStrings.FirstOrDefault();
2025-05-18 14:06:25 +02:00
}
2026-03-21 00:58:01 +01:00
else
{
2026-03-21 00:58:01 +01:00
SelectedStartTimeString = _previousSelectedStartTimeString;
SelectedEndTimeString = _previousSelectedEndTimeString;
2025-05-18 14:06:25 +02:00
}
}
2026-03-21 00:58:01 +01:00
partial void OnSelectedStartTimeStringChanged(string oldValue, string newValue)
2025-05-18 14:06:25 +02:00
{
2026-03-21 00:58:01 +01:00
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
2026-03-11 19:26:37 +01:00
2026-03-21 00:58:01 +01:00
if (parsedTime == null)
2025-05-18 14:06:25 +02:00
{
SelectedStartTimeString = _previousSelectedStartTimeString;
2025-01-01 17:28:29 +01:00
}
2026-03-16 21:41:22 +01:00
else if (!IsAllDay)
2025-05-18 14:06:25 +02:00
{
_previousSelectedStartTimeString = newValue;
}
}
2025-01-01 17:28:29 +01:00
2025-12-26 20:46:48 +01:00
partial void OnSelectedEndTimeStringChanged(string oldValue, string newValue)
2025-05-18 14:06:25 +02:00
{
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
if (parsedTime == null)
2024-11-10 23:28:25 +01:00
{
2026-03-16 21:41:22 +01:00
SelectedEndTimeString = _previousSelectedEndTimeString;
2025-05-18 14:06:25 +02:00
}
2026-03-16 21:41:22 +01:00
else if (!IsAllDay)
2025-05-18 14:06:25 +02:00
{
_previousSelectedEndTimeString = newValue;
}
}
2024-11-10 23:28:25 +01:00
}