2026-03-21 00:58:01 +01:00
|
|
|
using System;
|
2026-04-07 16:48:46 +02:00
|
|
|
using System.Collections.ObjectModel;
|
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;
|
2026-02-20 10:03:16 +01:00
|
|
|
using Itenso.TimePeriod;
|
2024-12-30 01:15:31 +01:00
|
|
|
using Serilog;
|
2024-12-29 19:37:36 +01:00
|
|
|
using Wino.Calendar.ViewModels.Data;
|
2024-12-29 17:41:54 +01:00
|
|
|
using Wino.Calendar.ViewModels.Interfaces;
|
2025-01-02 00:18:34 +01:00
|
|
|
using Wino.Calendar.ViewModels.Messages;
|
2026-03-08 13:21:42 +01:00
|
|
|
using Wino.Core.Domain;
|
2024-12-30 23:10:51 +01:00
|
|
|
using Wino.Core.Domain.Entities.Calendar;
|
2026-04-07 16:48:46 +02:00
|
|
|
using Wino.Core.Domain.Extensions;
|
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;
|
2024-12-29 17:41:54 +01:00
|
|
|
using Wino.Core.Domain.Models.Navigation;
|
2026-04-07 16:48:46 +02:00
|
|
|
using Wino.Core.Services;
|
2024-11-10 23:28:25 +01:00
|
|
|
using Wino.Core.ViewModels;
|
|
|
|
|
using Wino.Messaging.Client.Calendar;
|
2026-02-16 01:56:22 +01:00
|
|
|
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>,
|
2026-02-16 01:56:22 +01:00
|
|
|
IRecipient<CalendarItemRightTappedMessage>,
|
2026-04-08 23:46:55 +02:00
|
|
|
IRecipient<CalendarItemContextActionRequestedMessage>,
|
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 ||
|
2026-04-14 17:52:38 +02:00
|
|
|
SelectedQuickEventAccountCalendar.IsReadOnly ||
|
2026-03-16 21:41:22 +01:00
|
|
|
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-01-06 02:15:21 +01:00
|
|
|
|
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]
|
2026-04-07 16:48:46 +02:00
|
|
|
public partial ObservableCollection<CalendarItemViewModel> CalendarItems { get; set; } = new();
|
2026-03-23 10:22:47 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
#endregion
|
2025-01-06 02:15:21 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
#region Event Details
|
2025-01-06 02:15:21 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
public event EventHandler DetailsShowCalendarItemChanged;
|
2025-01-06 02:15:21 +01:00
|
|
|
|
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))]
|
2026-01-06 12:54:47 +01:00
|
|
|
[NotifyPropertyChangedFor(nameof(CanJoinOnline))]
|
2026-01-06 12:07:22 +01:00
|
|
|
public partial CalendarItemViewModel DisplayDetailsCalendarItemViewModel { get; set; }
|
2025-01-06 02:15:21 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
public bool IsEventDetailsVisible => DisplayDetailsCalendarItemViewModel != null;
|
2025-01-06 02:15:21 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
#endregion
|
2025-01-06 02:15:21 +01:00
|
|
|
|
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;
|
2026-04-11 01:28:19 +02:00
|
|
|
private readonly INotificationBuilder _notificationBuilder;
|
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-04-11 01:28:19 +02:00
|
|
|
private bool _isCalendarBadgeClearedForPageLifetime;
|
2026-04-07 16:48:46 +02:00
|
|
|
private Dictionary<Guid, CalendarItemViewModel> _loadedCalendarItems = new();
|
2025-01-06 02:15:21 +01:00
|
|
|
|
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,
|
2026-04-11 01:28:19 +02:00
|
|
|
INotificationBuilder notificationBuilder,
|
2026-03-21 00:58:01 +01:00
|
|
|
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;
|
2026-04-11 01:28:19 +02:00
|
|
|
_notificationBuilder = notificationBuilder;
|
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;
|
2024-12-29 22:30:00 +01:00
|
|
|
|
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;
|
|
|
|
|
|
2026-04-14 17:52:38 +02:00
|
|
|
if (DisplayDetailsCalendarItemViewModel.AssignedCalendar?.IsReadOnly == true)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.ShowReadOnlyCalendarMessage();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 13:21:42 +01:00
|
|
|
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);
|
2026-04-08 23:46:55 +02:00
|
|
|
Messenger.Unregister<CalendarItemContextActionRequestedMessage>(this);
|
2026-02-16 01:56:22 +01:00
|
|
|
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);
|
2026-04-08 23:46:55 +02:00
|
|
|
Messenger.Register<CalendarItemContextActionRequestedMessage>(this);
|
2026-02-19 01:37:43 +01:00
|
|
|
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);
|
2026-04-08 23:46:55 +02:00
|
|
|
Messenger.Unregister<CalendarItemContextActionRequestedMessage>(this);
|
2026-03-11 19:26:37 +01:00
|
|
|
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();
|
|
|
|
|
}
|
2024-12-30 01:15:31 +01:00
|
|
|
|
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();
|
|
|
|
|
}
|
2024-12-29 22:30:00 +01:00
|
|
|
|
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;
|
2026-04-07 16:48:46 +02:00
|
|
|
_loadedCalendarItems = new();
|
|
|
|
|
CalendarItems = new();
|
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();
|
2026-04-11 01:28:19 +02:00
|
|
|
_isCalendarBadgeClearedForPageLifetime = false;
|
2026-03-11 19:26:37 +01:00
|
|
|
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-01-16 22:00:05 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Series);
|
|
|
|
|
}
|
2025-01-06 02:15:21 +01:00
|
|
|
|
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-01-06 02:15:21 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
NavigateEvent(DisplayDetailsCalendarItemViewModel, CalendarEventTargetType.Single);
|
|
|
|
|
}
|
2025-01-06 02:15:21 +01:00
|
|
|
|
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-04-14 17:52:38 +02:00
|
|
|
if (SelectedQuickEventAccountCalendar?.IsReadOnly == true)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.ShowReadOnlyCalendarMessage();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
2026-04-08 23:46:02 +02:00
|
|
|
public async Task MoveCalendarItemAsync(CalendarItemViewModel calendarItemViewModel, DateTime targetStart)
|
|
|
|
|
{
|
|
|
|
|
if (calendarItemViewModel?.CalendarItem == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var calendarItem = calendarItemViewModel.CalendarItem;
|
|
|
|
|
|
|
|
|
|
if (!calendarItem.CanChangeStartAndEndDate)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.CalendarDragDropMoveNotAllowedTitle,
|
|
|
|
|
Translator.CalendarDragDropMoveNotAllowedMessage,
|
|
|
|
|
InfoBarMessageType.Warning);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 17:52:38 +02:00
|
|
|
if (calendarItem.AssignedCalendar?.IsReadOnly == true)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.ShowReadOnlyCalendarMessage();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 23:46:02 +02:00
|
|
|
var normalizedTargetStart = calendarItem.IsAllDayEvent
|
|
|
|
|
? targetStart.Date
|
|
|
|
|
: targetStart;
|
|
|
|
|
var targetEnd = normalizedTargetStart.AddSeconds(calendarItem.DurationInSeconds);
|
|
|
|
|
var currentLocalStart = calendarItem.LocalStartDate;
|
|
|
|
|
var currentLocalEnd = calendarItem.LocalEndDate;
|
|
|
|
|
|
|
|
|
|
if (currentLocalStart == normalizedTargetStart && currentLocalEnd == targetEnd)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var originalItem = CloneCalendarItem(calendarItem);
|
|
|
|
|
var attendees = await _calendarService.GetAttendeesAsync(calendarItem.EventTrackingId).ConfigureAwait(false) ?? [];
|
|
|
|
|
var originalAttendees = CloneAttendees(attendees);
|
|
|
|
|
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
calendarItemViewModel.StartDate = normalizedTargetStart;
|
|
|
|
|
calendarItemViewModel.DurationInSeconds = calendarItem.DurationInSeconds;
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
await _calendarService.UpdateCalendarItemAsync(calendarItem, attendees).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
var preparationRequest = new CalendarOperationPreparationRequest(
|
|
|
|
|
CalendarSynchronizerOperation.ChangeStartAndEndDate,
|
|
|
|
|
calendarItem,
|
|
|
|
|
attendees,
|
|
|
|
|
ResponseMessage: null,
|
|
|
|
|
OriginalItem: originalItem,
|
|
|
|
|
OriginalAttendees: originalAttendees);
|
|
|
|
|
|
|
|
|
|
await _winoRequestDelegator.ExecuteAsync(preparationRequest).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
partial void OnDisplayDetailsCalendarItemViewModelChanged(CalendarItemViewModel value)
|
|
|
|
|
=> DetailsShowCalendarItemChanged?.Invoke(this, EventArgs.Empty);
|
2025-01-06 02:15:21 +01:00
|
|
|
|
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
|
|
|
}
|
2024-12-29 17:41:54 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
HourSelectionStrings = timeStrings;
|
2025-01-07 20:51:10 +01:00
|
|
|
|
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);
|
2026-04-11 01:28:19 +02:00
|
|
|
var loadSucceeded = false;
|
2026-03-11 19:26:37 +01:00
|
|
|
|
|
|
|
|
if (!hasLoadingLock)
|
|
|
|
|
return;
|
2025-01-07 22:12:54 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
try
|
|
|
|
|
{
|
2026-03-11 19:26:37 +01:00
|
|
|
await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => IsCalendarEnabled = false).ConfigureAwait(false);
|
2025-01-06 02:15:21 +01:00
|
|
|
|
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;
|
2024-12-28 16:39:43 +01:00
|
|
|
|
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
|
|
|
{
|
2026-04-07 16:48:46 +02:00
|
|
|
ReplaceLoadedCalendarItems(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-04-11 01:28:19 +02:00
|
|
|
|
|
|
|
|
loadSucceeded = true;
|
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
|
2024-12-30 01:15:31 +01:00
|
|
|
{
|
2026-03-11 19:26:37 +01:00
|
|
|
ReleaseCalendarLoadingLock();
|
|
|
|
|
await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => IsCalendarEnabled = true).ConfigureAwait(false);
|
2024-12-30 01:15:31 +01:00
|
|
|
}
|
2026-04-11 01:28:19 +02:00
|
|
|
|
|
|
|
|
if (loadSucceeded && !_isCalendarBadgeClearedForPageLifetime && IsPageActive(lifetimeVersion))
|
|
|
|
|
{
|
|
|
|
|
await _notificationBuilder.ClearCalendarTaskbarBadgeAsync().ConfigureAwait(false);
|
|
|
|
|
_isCalendarBadgeClearedForPageLifetime = true;
|
|
|
|
|
}
|
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;
|
2024-12-30 01:15:31 +01:00
|
|
|
|
2026-04-11 01:28:19 +02:00
|
|
|
RefreshSettings();
|
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-04-07 16:48:46 +02:00
|
|
|
var activeCalendars = AccountCalendarStateService.ActiveCalendars.ToList();
|
|
|
|
|
var pendingCalendarItemIds = await GetPendingCalendarItemIdsAsync(activeCalendars, lifetimeVersion).ConfigureAwait(false);
|
2024-12-30 01:15:31 +01:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
foreach (var calendarViewModel in activeCalendars)
|
2024-12-30 01:15:31 +01:00
|
|
|
{
|
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;
|
2024-12-30 01:15:31 +01:00
|
|
|
|
2026-03-21 00:58:01 +01:00
|
|
|
calendarItem.AssignedCalendar ??= calendarViewModel;
|
2024-12-30 01:15:31 +01:00
|
|
|
|
2026-03-21 00:58:01 +01:00
|
|
|
if (!loadedItems.ContainsKey(calendarItem.Id))
|
|
|
|
|
{
|
2026-04-07 16:48:46 +02:00
|
|
|
loadedItems.Add(calendarItem.Id, CreateCalendarItemViewModel(calendarItem, pendingCalendarItemIds));
|
2026-03-21 00:58:01 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
return loadedItems.Values
|
|
|
|
|
.OrderBy(item => item.StartDate)
|
|
|
|
|
.ThenBy(item => item.EndDate)
|
|
|
|
|
.ThenBy(item => item.Id)
|
|
|
|
|
.ToList();
|
2025-05-18 14:06:25 +02:00
|
|
|
}
|
2024-12-31 14:28:28 +01: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-04-08 23:46:55 +02:00
|
|
|
if (message.CalendarItemViewModel == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Receive(CalendarItemContextActionRequestedMessage message)
|
|
|
|
|
{
|
|
|
|
|
if (message.CalendarItemViewModel == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (message.Action.ActionType == CalendarContextMenuActionType.Open)
|
|
|
|
|
{
|
|
|
|
|
NavigateEvent(message.CalendarItemViewModel, message.Action.TargetType ?? CalendarEventTargetType.Single);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ = ExecuteContextActionAsync(message.CalendarItemViewModel, message.Action);
|
2026-03-21 00:58:01 +01:00
|
|
|
}
|
2024-12-29 17:41:54 +01:00
|
|
|
|
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-04-07 16:48:46 +02:00
|
|
|
protected override void OnCalendarItemDeleted(CalendarItem calendarItem, EntityUpdateSource source)
|
2026-03-21 00:58:01 +01:00
|
|
|
{
|
2026-04-07 16:48:46 +02:00
|
|
|
base.OnCalendarItemDeleted(calendarItem, source);
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
if (calendarItem == null)
|
|
|
|
|
return;
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
if (calendarItem.IsRecurringParent)
|
2026-03-21 00:58:01 +01:00
|
|
|
{
|
|
|
|
|
_ = ReloadCurrentVisibleRangeAsync();
|
2026-04-07 16:48:46 +02:00
|
|
|
return;
|
2026-03-20 13:26:16 +01:00
|
|
|
}
|
2026-04-07 16:48:46 +02:00
|
|
|
|
|
|
|
|
var existingItemId = FindLoadedCalendarItemId(calendarItem);
|
|
|
|
|
if (!existingItemId.HasValue)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
RemoveLoadedCalendarItem(existingItemId.Value, calendarItem);
|
2026-03-21 00:58:01 +01:00
|
|
|
}
|
2026-03-20 13:26:16 +01:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
protected override void OnCalendarItemUpdated(CalendarItem calendarItem, EntityUpdateSource source)
|
2026-03-21 00:58:01 +01:00
|
|
|
{
|
|
|
|
|
base.OnCalendarItemUpdated(calendarItem, source);
|
2026-04-07 16:48:46 +02:00
|
|
|
ApplyCalendarItemUpsert(calendarItem, source);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void OnCalendarItemAdded(CalendarItem calendarItem, EntityUpdateSource source)
|
|
|
|
|
{
|
|
|
|
|
base.OnCalendarItemAdded(calendarItem, source);
|
|
|
|
|
ApplyCalendarItemUpsert(calendarItem, source);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<HashSet<Guid>> GetPendingCalendarItemIdsAsync(IEnumerable<AccountCalendarViewModel> activeCalendars, long lifetimeVersion)
|
|
|
|
|
{
|
|
|
|
|
var pendingCalendarItemIds = new HashSet<Guid>();
|
|
|
|
|
var accountIds = activeCalendars
|
|
|
|
|
.Select(calendar => calendar.Account.Id)
|
|
|
|
|
.Where(accountId => accountId != Guid.Empty)
|
|
|
|
|
.Distinct()
|
|
|
|
|
.ToList();
|
2026-03-20 13:26:16 +01:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
foreach (var accountId in accountIds)
|
2026-03-20 13:26:16 +01:00
|
|
|
{
|
2026-04-07 16:48:46 +02:00
|
|
|
if (!IsPageActive(lifetimeVersion))
|
|
|
|
|
return pendingCalendarItemIds;
|
|
|
|
|
|
|
|
|
|
IWinoSynchronizerBase synchronizer;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(accountId).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (InvalidOperationException)
|
|
|
|
|
{
|
|
|
|
|
return pendingCalendarItemIds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (synchronizer == null)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
foreach (var pendingCalendarItemId in synchronizer.GetPendingCalendarOperationIds())
|
|
|
|
|
{
|
|
|
|
|
pendingCalendarItemIds.Add(pendingCalendarItemId);
|
|
|
|
|
}
|
2025-05-18 14:06:25 +02:00
|
|
|
}
|
2024-12-28 23:17:16 +01:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
return pendingCalendarItemIds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplyCalendarItemUpsert(CalendarItem calendarItem, EntityUpdateSource source)
|
|
|
|
|
{
|
|
|
|
|
if (calendarItem == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (calendarItem.IsRecurringParent)
|
2025-05-18 14:06:25 +02:00
|
|
|
{
|
2026-03-21 00:58:01 +01:00
|
|
|
_ = ReloadCurrentVisibleRangeAsync();
|
2026-04-07 16:48:46 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var existingItemId = FindLoadedCalendarItemId(calendarItem);
|
|
|
|
|
var shouldDisplay = ShouldDisplayCalendarItem(calendarItem);
|
|
|
|
|
|
|
|
|
|
if (!shouldDisplay)
|
|
|
|
|
{
|
|
|
|
|
if (existingItemId.HasValue)
|
|
|
|
|
{
|
|
|
|
|
RemoveLoadedCalendarItem(existingItemId.Value, calendarItem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var newViewModel = CreateCalendarItemViewModel(calendarItem, source);
|
|
|
|
|
|
|
|
|
|
if (existingItemId.HasValue)
|
|
|
|
|
{
|
|
|
|
|
ReplaceLoadedCalendarItem(existingItemId.Value, newViewModel);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
InsertLoadedCalendarItem(newViewModel);
|
2025-05-18 14:06:25 +02:00
|
|
|
}
|
2026-03-21 00:58:01 +01:00
|
|
|
}
|
2024-12-30 23:10:51 +01:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
private CalendarItemViewModel CreateCalendarItemViewModel(CalendarItem calendarItem, EntityUpdateSource source)
|
|
|
|
|
=> CreateCalendarItemViewModel(
|
|
|
|
|
calendarItem,
|
|
|
|
|
source == EntityUpdateSource.ClientUpdated ? new HashSet<Guid> { calendarItem.Id } : null,
|
|
|
|
|
source);
|
|
|
|
|
|
2026-04-08 23:46:02 +02:00
|
|
|
private static CalendarItem CloneCalendarItem(CalendarItem calendarItem)
|
|
|
|
|
=> new()
|
|
|
|
|
{
|
|
|
|
|
Id = calendarItem.Id,
|
|
|
|
|
RemoteEventId = calendarItem.RemoteEventId,
|
|
|
|
|
Title = calendarItem.Title,
|
|
|
|
|
Description = calendarItem.Description,
|
|
|
|
|
Location = calendarItem.Location,
|
|
|
|
|
StartDate = calendarItem.StartDate,
|
|
|
|
|
StartTimeZone = calendarItem.StartTimeZone,
|
|
|
|
|
EndTimeZone = calendarItem.EndTimeZone,
|
|
|
|
|
DurationInSeconds = calendarItem.DurationInSeconds,
|
|
|
|
|
Recurrence = calendarItem.Recurrence,
|
|
|
|
|
OrganizerDisplayName = calendarItem.OrganizerDisplayName,
|
|
|
|
|
OrganizerEmail = calendarItem.OrganizerEmail,
|
|
|
|
|
RecurringCalendarItemId = calendarItem.RecurringCalendarItemId,
|
|
|
|
|
IsLocked = calendarItem.IsLocked,
|
|
|
|
|
IsHidden = calendarItem.IsHidden,
|
|
|
|
|
CustomEventColorHex = calendarItem.CustomEventColorHex,
|
|
|
|
|
HtmlLink = calendarItem.HtmlLink,
|
|
|
|
|
SnoozedUntil = calendarItem.SnoozedUntil,
|
|
|
|
|
Status = calendarItem.Status,
|
|
|
|
|
Visibility = calendarItem.Visibility,
|
|
|
|
|
ShowAs = calendarItem.ShowAs,
|
|
|
|
|
CreatedAt = calendarItem.CreatedAt,
|
|
|
|
|
UpdatedAt = calendarItem.UpdatedAt,
|
|
|
|
|
CalendarId = calendarItem.CalendarId,
|
|
|
|
|
AssignedCalendar = calendarItem.AssignedCalendar
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private static List<CalendarEventAttendee> CloneAttendees(IEnumerable<CalendarEventAttendee> attendees)
|
|
|
|
|
=> attendees?.Select(attendee => new CalendarEventAttendee
|
|
|
|
|
{
|
|
|
|
|
Id = attendee.Id,
|
|
|
|
|
CalendarItemId = attendee.CalendarItemId,
|
|
|
|
|
Name = attendee.Name,
|
|
|
|
|
Email = attendee.Email,
|
|
|
|
|
AttendenceStatus = attendee.AttendenceStatus,
|
|
|
|
|
IsOrganizer = attendee.IsOrganizer,
|
|
|
|
|
IsOptionalAttendee = attendee.IsOptionalAttendee,
|
|
|
|
|
Comment = attendee.Comment,
|
|
|
|
|
ResolvedContact = attendee.ResolvedContact
|
|
|
|
|
}).ToList() ?? [];
|
|
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
private CalendarItemViewModel CreateCalendarItemViewModel(CalendarItem calendarItem, ISet<Guid> pendingCalendarItemIds, EntityUpdateSource source = EntityUpdateSource.Server)
|
2026-03-21 00:58:01 +01:00
|
|
|
{
|
2026-04-07 16:48:46 +02:00
|
|
|
calendarItem.AssignedCalendar ??= ResolveAssignedCalendar(calendarItem.CalendarId);
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
return new CalendarItemViewModel(calendarItem)
|
2025-05-18 14:06:25 +02:00
|
|
|
{
|
2026-04-07 16:48:46 +02:00
|
|
|
IsBusy = source == EntityUpdateSource.ClientUpdated || HasPendingCalendarOperation(calendarItem, pendingCalendarItemIds)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ReplaceLoadedCalendarItems(IEnumerable<CalendarItemViewModel> loadedItems)
|
|
|
|
|
{
|
|
|
|
|
var loadedItemsList = loadedItems?.ToList() ?? [];
|
|
|
|
|
CalendarItems = new ObservableCollection<CalendarItemViewModel>(loadedItemsList);
|
|
|
|
|
_loadedCalendarItems = loadedItemsList.ToDictionary(item => item.Id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void InsertLoadedCalendarItem(CalendarItemViewModel calendarItemViewModel)
|
|
|
|
|
{
|
|
|
|
|
var insertionIndex = 0;
|
|
|
|
|
|
|
|
|
|
while (insertionIndex < CalendarItems.Count && CompareCalendarItems(CalendarItems[insertionIndex], calendarItemViewModel) <= 0)
|
|
|
|
|
{
|
|
|
|
|
insertionIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CalendarItems.Insert(insertionIndex, calendarItemViewModel);
|
|
|
|
|
_loadedCalendarItems[calendarItemViewModel.Id] = calendarItemViewModel;
|
|
|
|
|
|
|
|
|
|
if (IsDisplayDetailsMatch(calendarItemViewModel.CalendarItem))
|
|
|
|
|
{
|
|
|
|
|
DisplayDetailsCalendarItemViewModel = calendarItemViewModel;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ReplaceLoadedCalendarItem(Guid existingItemId, CalendarItemViewModel replacementViewModel)
|
|
|
|
|
{
|
|
|
|
|
if (!_loadedCalendarItems.TryGetValue(existingItemId, out var existingViewModel))
|
|
|
|
|
{
|
|
|
|
|
InsertLoadedCalendarItem(replacementViewModel);
|
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-04-07 16:48:46 +02:00
|
|
|
replacementViewModel.IsSelected = existingViewModel.IsSelected;
|
|
|
|
|
|
|
|
|
|
var existingIndex = CalendarItems.IndexOf(existingViewModel);
|
|
|
|
|
if (existingIndex >= 0)
|
2025-05-18 14:06:25 +02:00
|
|
|
{
|
2026-04-07 16:48:46 +02:00
|
|
|
CalendarItems[existingIndex] = replacementViewModel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_loadedCalendarItems.Remove(existingItemId);
|
|
|
|
|
_loadedCalendarItems[replacementViewModel.Id] = replacementViewModel;
|
|
|
|
|
|
|
|
|
|
if (existingIndex >= 0)
|
|
|
|
|
{
|
|
|
|
|
MoveCalendarItemToSortedPosition(replacementViewModel, existingIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (IsDisplayDetailsMatch(replacementViewModel.CalendarItem, existingItemId))
|
|
|
|
|
{
|
|
|
|
|
DisplayDetailsCalendarItemViewModel = replacementViewModel;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RemoveLoadedCalendarItem(Guid existingItemId, CalendarItem calendarItem)
|
|
|
|
|
{
|
|
|
|
|
if (_loadedCalendarItems.TryGetValue(existingItemId, out var existingViewModel))
|
|
|
|
|
{
|
|
|
|
|
CalendarItems.Remove(existingViewModel);
|
|
|
|
|
_loadedCalendarItems.Remove(existingItemId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (IsDisplayDetailsMatch(calendarItem, existingItemId))
|
|
|
|
|
{
|
|
|
|
|
DisplayDetailsCalendarItemViewModel = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void MoveCalendarItemToSortedPosition(CalendarItemViewModel calendarItemViewModel, int previousIndex)
|
|
|
|
|
{
|
|
|
|
|
if (previousIndex < 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var targetIndex = 0;
|
|
|
|
|
while (targetIndex < CalendarItems.Count && CompareCalendarItems(CalendarItems[targetIndex], calendarItemViewModel) <= 0)
|
|
|
|
|
{
|
|
|
|
|
targetIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (targetIndex > previousIndex)
|
|
|
|
|
{
|
|
|
|
|
targetIndex--;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (targetIndex != previousIndex)
|
|
|
|
|
{
|
|
|
|
|
CalendarItems.Move(previousIndex, targetIndex);
|
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-04-07 16:48:46 +02:00
|
|
|
private Guid? FindLoadedCalendarItemId(CalendarItem calendarItem)
|
|
|
|
|
{
|
|
|
|
|
if (calendarItem == null)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
if (_loadedCalendarItems.ContainsKey(calendarItem.Id))
|
|
|
|
|
return calendarItem.Id;
|
|
|
|
|
|
|
|
|
|
var trackedLocalItemId = calendarItem.RemoteEventId.GetClientTrackingId();
|
|
|
|
|
if (trackedLocalItemId.HasValue && _loadedCalendarItems.ContainsKey(trackedLocalItemId.Value))
|
|
|
|
|
return trackedLocalItemId.Value;
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool ShouldDisplayCalendarItem(CalendarItem calendarItem)
|
2026-03-21 00:58:01 +01:00
|
|
|
{
|
|
|
|
|
if (calendarItem == null || LoadedDateWindow == null)
|
|
|
|
|
return false;
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
if (calendarItem.IsHidden || calendarItem.IsRecurringParent || !IsCalendarActive(calendarItem.CalendarId))
|
|
|
|
|
return false;
|
|
|
|
|
|
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-04-07 16:48:46 +02:00
|
|
|
private bool IsDisplayDetailsMatch(CalendarItem calendarItem, Guid? existingItemId = null)
|
|
|
|
|
{
|
|
|
|
|
if (DisplayDetailsCalendarItemViewModel == null || calendarItem == null)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
var trackedLocalItemId = calendarItem.RemoteEventId.GetClientTrackingId();
|
|
|
|
|
|
|
|
|
|
return DisplayDetailsCalendarItemViewModel.Id == calendarItem.Id ||
|
|
|
|
|
(existingItemId.HasValue && DisplayDetailsCalendarItemViewModel.Id == existingItemId.Value) ||
|
|
|
|
|
(trackedLocalItemId.HasValue && DisplayDetailsCalendarItemViewModel.Id == trackedLocalItemId.Value) ||
|
|
|
|
|
DisplayDetailsCalendarItemViewModel.CalendarItem?.RecurringCalendarItemId == calendarItem.Id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool HasPendingCalendarOperation(CalendarItem calendarItem, ISet<Guid> pendingCalendarItemIds)
|
|
|
|
|
{
|
|
|
|
|
if (calendarItem == null || pendingCalendarItemIds == null || pendingCalendarItemIds.Count == 0)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
if (pendingCalendarItemIds.Contains(calendarItem.Id))
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
var trackedLocalItemId = calendarItem.RemoteEventId.GetClientTrackingId();
|
|
|
|
|
return trackedLocalItemId.HasValue && pendingCalendarItemIds.Contains(trackedLocalItemId.Value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 23:46:55 +02:00
|
|
|
private async Task ExecuteContextActionAsync(CalendarItemViewModel calendarItemViewModel, CalendarContextMenuAction action)
|
|
|
|
|
{
|
|
|
|
|
switch (action.ActionType)
|
|
|
|
|
{
|
|
|
|
|
case CalendarContextMenuActionType.JoinOnline:
|
|
|
|
|
await JoinOnlineAsync(calendarItemViewModel).ConfigureAwait(false);
|
|
|
|
|
break;
|
|
|
|
|
case CalendarContextMenuActionType.Delete:
|
|
|
|
|
await DeleteCalendarItemAsync(calendarItemViewModel, action.TargetType ?? CalendarEventTargetType.Single).ConfigureAwait(false);
|
|
|
|
|
break;
|
|
|
|
|
case CalendarContextMenuActionType.ShowAs when action.ShowAs.HasValue:
|
|
|
|
|
await UpdateShowAsAsync(calendarItemViewModel, action.TargetType ?? CalendarEventTargetType.Single, action.ShowAs.Value).ConfigureAwait(false);
|
|
|
|
|
break;
|
|
|
|
|
case CalendarContextMenuActionType.Respond when action.ResponseStatus.HasValue:
|
|
|
|
|
await RespondToCalendarItemAsync(calendarItemViewModel, action.TargetType ?? CalendarEventTargetType.Single, action.ResponseStatus.Value).ConfigureAwait(false);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Task JoinOnlineAsync(CalendarItemViewModel calendarItemViewModel)
|
|
|
|
|
{
|
|
|
|
|
var htmlLink = calendarItemViewModel?.CalendarItem?.HtmlLink;
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(htmlLink))
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
|
|
|
|
|
return _nativeAppService.LaunchUriAsync(new Uri(htmlLink));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task DeleteCalendarItemAsync(CalendarItemViewModel calendarItemViewModel, CalendarEventTargetType targetType)
|
|
|
|
|
{
|
|
|
|
|
var targetItem = await ResolveCalendarItemTargetAsync(calendarItemViewModel, targetType).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (targetItem == null)
|
|
|
|
|
return;
|
|
|
|
|
|
2026-04-14 17:52:38 +02:00
|
|
|
if (targetItem.AssignedCalendar?.IsReadOnly == true)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.ShowReadOnlyCalendarMessage();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 23:46:55 +02:00
|
|
|
if (targetItem.IsRecurringParent)
|
|
|
|
|
{
|
|
|
|
|
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
|
|
|
|
|
Translator.DialogMessage_DeleteRecurringSeriesMessage,
|
|
|
|
|
Translator.DialogMessage_DeleteRecurringSeriesTitle,
|
|
|
|
|
Translator.Buttons_Delete).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (!confirmed)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var preparationRequest = new CalendarOperationPreparationRequest(
|
|
|
|
|
CalendarSynchronizerOperation.DeleteEvent,
|
|
|
|
|
targetItem,
|
|
|
|
|
null);
|
|
|
|
|
|
|
|
|
|
await _winoRequestDelegator.ExecuteAsync(preparationRequest).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task UpdateShowAsAsync(CalendarItemViewModel calendarItemViewModel, CalendarEventTargetType targetType, CalendarItemShowAs showAs)
|
|
|
|
|
{
|
|
|
|
|
var targetItem = await ResolveCalendarItemTargetAsync(calendarItemViewModel, targetType).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (targetItem == null || targetItem.ShowAs == showAs)
|
|
|
|
|
return;
|
|
|
|
|
|
2026-04-14 17:52:38 +02:00
|
|
|
if (targetItem.AssignedCalendar?.IsReadOnly == true)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.ShowReadOnlyCalendarMessage();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 23:46:55 +02:00
|
|
|
var originalItem = await _calendarService.GetCalendarItemAsync(targetItem.Id).ConfigureAwait(false);
|
|
|
|
|
var attendees = await _calendarService.GetAttendeesAsync(targetItem.Id).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
targetItem.ShowAs = showAs;
|
|
|
|
|
await _calendarService.UpdateCalendarItemAsync(targetItem, attendees).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
var preparationRequest = new CalendarOperationPreparationRequest(
|
|
|
|
|
CalendarSynchronizerOperation.UpdateEvent,
|
|
|
|
|
targetItem,
|
|
|
|
|
attendees,
|
|
|
|
|
ResponseMessage: null,
|
|
|
|
|
OriginalItem: originalItem,
|
|
|
|
|
OriginalAttendees: attendees);
|
|
|
|
|
|
|
|
|
|
await _winoRequestDelegator.ExecuteAsync(preparationRequest).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task RespondToCalendarItemAsync(CalendarItemViewModel calendarItemViewModel, CalendarEventTargetType targetType, CalendarItemStatus responseStatus)
|
|
|
|
|
{
|
|
|
|
|
var targetItem = await ResolveCalendarItemTargetAsync(calendarItemViewModel, targetType).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (targetItem == null)
|
|
|
|
|
return;
|
|
|
|
|
|
2026-04-14 17:52:38 +02:00
|
|
|
if (targetItem.AssignedCalendar?.IsReadOnly == true)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.ShowReadOnlyCalendarMessage();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 23:46:55 +02:00
|
|
|
var operation = responseStatus switch
|
|
|
|
|
{
|
|
|
|
|
CalendarItemStatus.Accepted => CalendarSynchronizerOperation.AcceptEvent,
|
|
|
|
|
CalendarItemStatus.Tentative => CalendarSynchronizerOperation.TentativeEvent,
|
|
|
|
|
CalendarItemStatus.Cancelled => CalendarSynchronizerOperation.DeclineEvent,
|
|
|
|
|
_ => throw new InvalidOperationException($"Unsupported calendar response status '{responseStatus}'.")
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var preparationRequest = new CalendarOperationPreparationRequest(
|
|
|
|
|
operation,
|
|
|
|
|
targetItem,
|
|
|
|
|
null);
|
|
|
|
|
|
|
|
|
|
await _winoRequestDelegator.ExecuteAsync(preparationRequest).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<CalendarItem> ResolveCalendarItemTargetAsync(CalendarItemViewModel calendarItemViewModel, CalendarEventTargetType targetType)
|
|
|
|
|
{
|
|
|
|
|
if (calendarItemViewModel?.CalendarItem == null)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
var target = new CalendarItemTarget(calendarItemViewModel.CalendarItem, targetType);
|
|
|
|
|
var targetItem = await _calendarService.GetCalendarItemTargetAsync(target).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
targetItem ??= calendarItemViewModel.CalendarItem;
|
|
|
|
|
if (targetItem == calendarItemViewModel.CalendarItem || targetItem.AssignedCalendar == null)
|
|
|
|
|
{
|
|
|
|
|
targetItem.AssignedCalendar = await _calendarService.GetAccountCalendarAsync(targetItem.CalendarId).ConfigureAwait(false)
|
|
|
|
|
?? calendarItemViewModel.AssignedCalendar
|
|
|
|
|
?? ResolveAssignedCalendar(targetItem.CalendarId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return targetItem;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 16:48:46 +02:00
|
|
|
private AccountCalendarViewModel ResolveAssignedCalendar(Guid calendarId)
|
|
|
|
|
=> AccountCalendarStateService.AllCalendars.FirstOrDefault(calendar => calendar.Id == calendarId);
|
|
|
|
|
|
|
|
|
|
private static int CompareCalendarItems(CalendarItemViewModel left, CalendarItemViewModel right)
|
|
|
|
|
{
|
|
|
|
|
var compareResult = DateTime.Compare(left?.StartDate ?? DateTime.MinValue, right?.StartDate ?? DateTime.MinValue);
|
|
|
|
|
if (compareResult != 0)
|
|
|
|
|
return compareResult;
|
|
|
|
|
|
|
|
|
|
compareResult = DateTime.Compare(left?.EndDate ?? DateTime.MinValue, right?.EndDate ?? DateTime.MinValue);
|
|
|
|
|
if (compareResult != 0)
|
|
|
|
|
return compareResult;
|
|
|
|
|
|
|
|
|
|
return Nullable.Compare(left?.Id, right?.Id);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
2024-12-30 23:10:51 +01:00
|
|
|
{
|
2026-03-21 00:58:01 +01:00
|
|
|
SelectedStartTimeString = _previousSelectedStartTimeString;
|
|
|
|
|
SelectedEndTimeString = _previousSelectedEndTimeString;
|
2025-05-18 14:06:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
2024-12-30 01:15:31 +01: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
|
|
|
}
|