2026-03-08 11:22:41 +01:00
|
|
|
using System;
|
2024-12-29 17:41:54 +01:00
|
|
|
using System.Collections.Generic;
|
2024-11-10 23:28:25 +01:00
|
|
|
using System.Globalization;
|
2026-03-06 17:46:38 +01:00
|
|
|
using System.Linq;
|
2024-12-29 19:37:36 +01:00
|
|
|
using System.Threading;
|
2024-11-10 23:28:25 +01:00
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
|
|
|
|
using CommunityToolkit.Mvvm.Messaging;
|
2024-12-29 19:37:36 +01:00
|
|
|
using Serilog;
|
2024-12-29 17:41:54 +01:00
|
|
|
using Wino.Calendar.ViewModels.Data;
|
2026-03-07 01:46:07 +01:00
|
|
|
using Wino.Core.Domain.Entities.Calendar;
|
2024-12-29 17:41:54 +01:00
|
|
|
using Wino.Calendar.ViewModels.Interfaces;
|
2026-03-06 17:46:38 +01:00
|
|
|
using Wino.Core.Domain;
|
2024-11-30 23:05:07 +01:00
|
|
|
using Wino.Core.Domain.Collections;
|
2024-11-10 23:28:25 +01:00
|
|
|
using Wino.Core.Domain.Enums;
|
2025-01-06 21:56:33 +01:00
|
|
|
using Wino.Core.Domain.Extensions;
|
2024-11-10 23:28:25 +01:00
|
|
|
using Wino.Core.Domain.Interfaces;
|
2026-03-10 16:50:16 +01:00
|
|
|
using Wino.Core.Domain.MenuItems;
|
2024-11-10 23:28:25 +01:00
|
|
|
using Wino.Core.Domain.Models.Calendar;
|
2026-03-08 13:21:42 +01:00
|
|
|
using Wino.Core.Domain.Models;
|
2024-11-10 23:28:25 +01:00
|
|
|
using Wino.Core.Domain.Models.Navigation;
|
2024-12-27 00:18:46 +01:00
|
|
|
using Wino.Core.Domain.Models.Synchronization;
|
2024-11-10 23:28:25 +01:00
|
|
|
using Wino.Core.ViewModels;
|
|
|
|
|
using Wino.Messaging.Client.Calendar;
|
|
|
|
|
using Wino.Messaging.Client.Navigation;
|
2024-12-27 00:18:46 +01:00
|
|
|
using Wino.Messaging.Server;
|
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;
|
|
|
|
|
|
2025-12-26 20:46:48 +01:00
|
|
|
public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
2026-03-11 01:39:32 +01:00
|
|
|
ICalendarShellClient,
|
2025-05-18 14:06:25 +02:00
|
|
|
IRecipient<VisibleDateRangeChangedMessage>,
|
|
|
|
|
IRecipient<CalendarEnableStatusChangedMessage>,
|
|
|
|
|
IRecipient<NavigateManageAccountsRequested>,
|
2026-02-16 01:56:22 +01:00
|
|
|
IRecipient<CalendarDisplayTypeChangedMessage>,
|
|
|
|
|
IRecipient<AccountRemovedMessage>
|
2024-11-10 23:28:25 +01:00
|
|
|
{
|
2025-05-18 14:06:25 +02:00
|
|
|
public IPreferencesService PreferencesService { get; }
|
|
|
|
|
public IStatePersistanceService StatePersistenceService { get; }
|
|
|
|
|
public IAccountCalendarStateService AccountCalendarStateService { get; }
|
|
|
|
|
public INavigationService NavigationService { get; }
|
2026-03-11 01:39:32 +01:00
|
|
|
public WinoApplicationMode Mode => WinoApplicationMode.Calendar;
|
|
|
|
|
public bool HandlesNavigationSelection => false;
|
|
|
|
|
System.Collections.IEnumerable ICalendarShellClient.GroupedAccountCalendars => AccountCalendarStateService.GroupedAccountCalendars;
|
|
|
|
|
System.Collections.IEnumerable ICalendarShellClient.DateNavigationHeaderItems => DateNavigationHeaderItems;
|
|
|
|
|
object IShellClient.SelectedMenuItem
|
|
|
|
|
{
|
|
|
|
|
get => null;
|
|
|
|
|
set { }
|
|
|
|
|
}
|
|
|
|
|
System.Windows.Input.ICommand ICalendarShellClient.TodayClickedCommand => TodayClickedCommand;
|
|
|
|
|
System.Windows.Input.ICommand ICalendarShellClient.DateClickedCommand => DateClickedCommand;
|
2025-05-18 14:06:25 +02:00
|
|
|
|
2026-03-10 16:50:16 +01:00
|
|
|
public MenuItemCollection MenuItems { get; private set; }
|
|
|
|
|
public MenuItemCollection FooterItems { get; private set; }
|
|
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private int _selectedMenuItemIndex = -1;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isCalendarEnabled;
|
|
|
|
|
|
2025-10-03 21:55:23 +02:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the display date of the calendar.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private DateTimeOffset _displayDate;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the highlighted range in the CalendarView and displayed date range in FlipView.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private DateRange highlightedDateRange;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private ObservableRangeCollection<string> dateNavigationHeaderItems = [];
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private int _selectedDateNavigationHeaderIndex;
|
|
|
|
|
|
|
|
|
|
public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month;
|
|
|
|
|
|
2026-03-08 11:22:41 +01:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isStoreUpdateItemVisible;
|
|
|
|
|
|
2026-03-10 16:50:16 +01:00
|
|
|
private readonly ManageAccountsMenuItem _manageAccountsMenuItem = new();
|
|
|
|
|
private readonly SettingsItem _settingsItem = new();
|
|
|
|
|
private readonly StoreUpdateMenuItem _storeUpdateMenuItem = new();
|
2026-03-11 01:39:32 +01:00
|
|
|
private readonly NewCalendarEventMenuItem _newEventMenuItem = new();
|
2026-03-10 16:50:16 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
// For updating account calendars asynchronously.
|
|
|
|
|
private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
|
|
|
|
|
|
2025-12-26 20:46:48 +01:00
|
|
|
public CalendarAppShellViewModel(IPreferencesService preferencesService,
|
2025-05-18 14:06:25 +02:00
|
|
|
IStatePersistanceService statePersistanceService,
|
|
|
|
|
IAccountService accountService,
|
|
|
|
|
ICalendarService calendarService,
|
|
|
|
|
IAccountCalendarStateService accountCalendarStateService,
|
2026-03-02 00:44:29 +01:00
|
|
|
INavigationService navigationService,
|
2026-03-06 17:46:38 +01:00
|
|
|
IMailDialogService dialogService,
|
2026-03-08 11:22:41 +01:00
|
|
|
IUpdateManager updateManager,
|
|
|
|
|
IStoreUpdateService storeUpdateService)
|
2024-11-10 23:28:25 +01:00
|
|
|
{
|
2025-05-18 14:06:25 +02:00
|
|
|
_accountService = accountService;
|
|
|
|
|
_calendarService = calendarService;
|
2026-03-02 00:44:29 +01:00
|
|
|
_dialogService = dialogService;
|
|
|
|
|
_updateManager = updateManager;
|
2026-03-08 11:22:41 +01:00
|
|
|
_storeUpdateService = storeUpdateService;
|
2024-12-29 17:41:54 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
AccountCalendarStateService = accountCalendarStateService;
|
|
|
|
|
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
|
|
|
|
|
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
|
2024-12-29 19:37:36 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
NavigationService = navigationService;
|
|
|
|
|
PreferencesService = preferencesService;
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
StatePersistenceService = statePersistanceService;
|
|
|
|
|
StatePersistenceService.StatePropertyChanged += PrefefencesChanged;
|
|
|
|
|
}
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2026-01-20 00:30:24 +01:00
|
|
|
protected override void OnDispatcherAssigned()
|
2025-05-18 14:06:25 +02:00
|
|
|
{
|
2026-01-20 00:30:24 +01:00
|
|
|
base.OnDispatcherAssigned();
|
|
|
|
|
|
|
|
|
|
AccountCalendarStateService.Dispatcher = Dispatcher;
|
2026-03-10 16:50:16 +01:00
|
|
|
MenuItems = new MenuItemCollection(Dispatcher);
|
|
|
|
|
FooterItems = new MenuItemCollection(Dispatcher);
|
|
|
|
|
MenuItems.Add(_newEventMenuItem);
|
2026-03-08 11:22:41 +01:00
|
|
|
_ = RefreshFooterItemsAsync(false);
|
2025-05-18 14:06:25 +02:00
|
|
|
}
|
2025-01-02 00:18:34 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
private void PrefefencesChanged(object sender, string e)
|
|
|
|
|
{
|
|
|
|
|
if (e == nameof(StatePersistenceService.CalendarDisplayType))
|
2024-11-10 23:28:25 +01:00
|
|
|
{
|
2025-05-18 14:06:25 +02:00
|
|
|
Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType));
|
2025-01-01 17:28:29 +01:00
|
|
|
|
2026-02-13 03:09:13 +01:00
|
|
|
UpdateDateNavigationHeaderItems();
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
// Change the calendar.
|
|
|
|
|
DateClicked(new CalendarViewDayClickedEventArgs(GetDisplayTypeSwitchDate()));
|
2024-11-10 23:28:25 +01:00
|
|
|
}
|
2025-05-18 14:06:25 +02:00
|
|
|
}
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2026-03-08 11:22:41 +01:00
|
|
|
private async void PreferencesServiceChanged(object sender, string e)
|
|
|
|
|
{
|
|
|
|
|
if (e == nameof(IPreferencesService.IsStoreUpdateNotificationsEnabled))
|
|
|
|
|
{
|
|
|
|
|
await RefreshFooterItemsAsync(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
|
|
|
|
{
|
|
|
|
|
base.OnNavigatedTo(mode, parameters);
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2026-03-11 01:39:32 +01:00
|
|
|
var activationContext = parameters as ShellModeActivationContext;
|
|
|
|
|
var isModeResetActivation = activationContext != null;
|
|
|
|
|
var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true;
|
|
|
|
|
|
2026-03-08 11:22:41 +01:00
|
|
|
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
|
|
|
|
PreferencesService.PreferenceChanged += PreferencesServiceChanged;
|
|
|
|
|
|
|
|
|
|
await RefreshFooterItemsAsync(mode == NavigationMode.New);
|
|
|
|
|
|
2026-03-08 01:33:47 +01:00
|
|
|
// Preserve the existing calendar shell frame state when the user switches
|
|
|
|
|
// between Mail and Calendar modes. Back/forward restoration should not
|
|
|
|
|
// force a new CalendarPage navigation, otherwise pages like
|
|
|
|
|
// CalendarEventComposePage get dropped from the inner frame stack.
|
2026-03-11 01:39:32 +01:00
|
|
|
if (mode != NavigationMode.New && !isModeResetActivation)
|
2026-02-16 01:56:22 +01:00
|
|
|
{
|
2026-03-08 01:33:47 +01:00
|
|
|
UpdateDateNavigationHeaderItems();
|
2026-02-16 01:56:22 +01:00
|
|
|
await InitializeAccountCalendarsAsync();
|
2026-03-07 01:46:07 +01:00
|
|
|
ValidateConfiguredNewEventCalendar();
|
2026-02-16 01:56:22 +01:00
|
|
|
return;
|
|
|
|
|
}
|
2025-12-27 19:16:24 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
UpdateDateNavigationHeaderItems();
|
2024-12-29 17:41:54 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
await InitializeAccountCalendarsAsync();
|
2026-03-07 01:46:07 +01:00
|
|
|
ValidateConfiguredNewEventCalendar();
|
2024-12-29 19:37:36 +01:00
|
|
|
|
2026-03-11 01:39:32 +01:00
|
|
|
if (shouldRunStartupFlows)
|
|
|
|
|
{
|
|
|
|
|
await ShowWhatIsNewIfNeededAsync();
|
|
|
|
|
}
|
2026-03-02 00:44:29 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
TodayClicked();
|
|
|
|
|
}
|
2024-12-29 17:41:54 +01:00
|
|
|
|
2026-03-08 11:22:41 +01:00
|
|
|
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
|
|
|
|
{
|
|
|
|
|
base.OnNavigatedFrom(mode, parameters);
|
|
|
|
|
|
|
|
|
|
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 00:44:29 +01:00
|
|
|
private async Task ShowWhatIsNewIfNeededAsync()
|
|
|
|
|
{
|
|
|
|
|
if (!_updateManager.ShouldShowUpdateNotes())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var notes = await _updateManager.GetLatestUpdateNotesAsync();
|
|
|
|
|
|
|
|
|
|
if (notes.Sections.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
await _dialogService.ShowWhatIsNewDialogAsync(notes);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 11:22:41 +01:00
|
|
|
private async Task RefreshFooterItemsAsync(bool showNotification)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
2026-03-10 16:50:16 +01:00
|
|
|
FooterItems.Clear();
|
2026-03-08 11:22:41 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task StartStoreUpdateAsync()
|
|
|
|
|
{
|
|
|
|
|
await _storeUpdateService.StartUpdateAsync().ConfigureAwait(false);
|
|
|
|
|
await RefreshFooterItemsAsync(false).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
|
|
|
|
|
{
|
|
|
|
|
// When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
|
|
|
|
|
// Reporting all these changes one by one to the UI is not efficient and may cause problems in the future.
|
2024-12-29 19:37:36 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
// Update all calendar states at once.
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await _accountCalendarUpdateSemaphoreSlim.WaitAsync();
|
2024-12-29 17:41:54 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
foreach (var calendar in e.AccountCalendars)
|
2024-12-29 19:37:36 +01:00
|
|
|
{
|
2025-05-18 14:06:25 +02:00
|
|
|
await _calendarService.UpdateAccountCalendarAsync(calendar.AccountCalendar).ConfigureAwait(false);
|
2024-12-29 19:37:36 +01:00
|
|
|
}
|
2024-12-29 17:41:54 +01:00
|
|
|
}
|
2025-05-18 14:06:25 +02:00
|
|
|
catch (Exception ex)
|
2024-12-29 17:41:54 +01:00
|
|
|
{
|
2025-05-18 14:06:25 +02:00
|
|
|
Log.Error(ex, "Error while waiting for account calendar update semaphore.");
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_accountCalendarUpdateSemaphoreSlim.Release();
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-12-29 17:41:54 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
private async void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
|
|
|
|
|
=> await _calendarService.UpdateAccountCalendarAsync(e.AccountCalendar).ConfigureAwait(false);
|
2024-12-29 17:41:54 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
private async Task InitializeAccountCalendarsAsync()
|
|
|
|
|
{
|
2025-12-30 10:02:24 +01:00
|
|
|
await Dispatcher.ExecuteOnUIThread(() => AccountCalendarStateService.ClearGroupedAccountCalendars());
|
2024-12-29 17:41:54 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
2024-12-29 17:41:54 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
foreach (var account in accounts)
|
|
|
|
|
{
|
|
|
|
|
var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
|
|
|
|
|
var calendarViewModels = new List<AccountCalendarViewModel>();
|
2024-12-29 17:41:54 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
foreach (var calendar in accountCalendars)
|
|
|
|
|
{
|
|
|
|
|
var calendarViewModel = new AccountCalendarViewModel(account, calendar);
|
2024-12-29 17:41:54 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
calendarViewModels.Add(calendarViewModel);
|
2024-12-29 17:41:54 +01:00
|
|
|
}
|
2025-05-18 14:06:25 +02:00
|
|
|
|
|
|
|
|
var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels);
|
|
|
|
|
|
|
|
|
|
await Dispatcher.ExecuteOnUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
AccountCalendarStateService.AddGroupedAccountCalendar(groupedAccountCalendarViewModel);
|
|
|
|
|
});
|
2024-11-10 23:28:25 +01:00
|
|
|
}
|
2025-05-18 14:06:25 +02:00
|
|
|
}
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
private void ForceNavigateCalendarDate()
|
|
|
|
|
{
|
2026-03-08 11:22:41 +01:00
|
|
|
var args = new CalendarPageNavigationArgs()
|
2024-12-28 16:39:43 +01:00
|
|
|
{
|
2026-03-08 11:22:41 +01:00
|
|
|
NavigationDate = _navigationDate ?? DateTime.Now.Date
|
|
|
|
|
};
|
2024-12-28 16:39:43 +01:00
|
|
|
|
2026-03-08 11:22:41 +01:00
|
|
|
NavigationService.Navigate(WinoPage.CalendarPage, args);
|
|
|
|
|
_navigationDate = null;
|
2025-05-18 14:06:25 +02:00
|
|
|
}
|
2024-12-27 00:18:46 +01:00
|
|
|
|
2026-03-10 16:50:16 +01:00
|
|
|
partial void OnSelectedMenuItemIndexChanged(int oldValue, int newValue) { }
|
2025-05-18 14:06:25 +02:00
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task Sync()
|
|
|
|
|
{
|
|
|
|
|
// Sync all calendars.
|
|
|
|
|
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
2025-01-07 14:38:45 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
foreach (var account in accounts)
|
|
|
|
|
{
|
|
|
|
|
var t = new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions()
|
2024-11-10 23:28:25 +01:00
|
|
|
{
|
2025-05-18 14:06:25 +02:00
|
|
|
AccountId = account.Id,
|
2026-02-18 20:43:10 +01:00
|
|
|
Type = CalendarSynchronizationType.CalendarEvents
|
2025-12-26 20:46:48 +01:00
|
|
|
});
|
2024-12-27 00:18:46 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
Messenger.Send(t);
|
2024-11-10 23:28:25 +01:00
|
|
|
}
|
2025-05-18 14:06:25 +02:00
|
|
|
}
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// When calendar type switches, we need to navigate to the most ideal date.
|
|
|
|
|
/// This method returns that date.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private DateTime GetDisplayTypeSwitchDate()
|
|
|
|
|
{
|
|
|
|
|
var settings = PreferencesService.GetCurrentCalendarSettings();
|
|
|
|
|
switch (StatePersistenceService.CalendarDisplayType)
|
2024-11-10 23:28:25 +01:00
|
|
|
{
|
2025-05-18 14:06:25 +02:00
|
|
|
case CalendarDisplayType.Day:
|
|
|
|
|
if (HighlightedDateRange.IsInRange(DateTime.Now)) return DateTime.Now.Date;
|
|
|
|
|
|
|
|
|
|
return HighlightedDateRange.StartDate;
|
|
|
|
|
case CalendarDisplayType.Week:
|
|
|
|
|
if (HighlightedDateRange == null || HighlightedDateRange.IsInRange(DateTime.Now))
|
|
|
|
|
{
|
|
|
|
|
return DateTime.Now.Date.GetWeekStartDateForDate(settings.FirstDayOfWeek);
|
|
|
|
|
}
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
return HighlightedDateRange.StartDate.GetWeekStartDateForDate(settings.FirstDayOfWeek);
|
|
|
|
|
case CalendarDisplayType.WorkWeek:
|
|
|
|
|
break;
|
|
|
|
|
case CalendarDisplayType.Month:
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
2024-11-10 23:28:25 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
return DateTime.Today.Date;
|
|
|
|
|
}
|
2024-12-28 16:39:43 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
private DateTime? _navigationDate;
|
|
|
|
|
private readonly IAccountService _accountService;
|
|
|
|
|
private readonly ICalendarService _calendarService;
|
2026-03-06 17:46:38 +01:00
|
|
|
private readonly IMailDialogService _dialogService;
|
2026-03-02 00:44:29 +01:00
|
|
|
private readonly IUpdateManager _updateManager;
|
2026-03-08 11:22:41 +01:00
|
|
|
private readonly IStoreUpdateService _storeUpdateService;
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
#region Commands
|
2024-12-28 16:39:43 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
[RelayCommand]
|
|
|
|
|
private void TodayClicked()
|
|
|
|
|
{
|
|
|
|
|
_navigationDate = DateTime.Now.Date;
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
ForceNavigateCalendarDate();
|
|
|
|
|
}
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
[RelayCommand]
|
|
|
|
|
public void ManageAccounts() => NavigationService.Navigate(WinoPage.AccountManagementPage);
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2026-03-10 16:50:16 +01:00
|
|
|
public async Task HandleNavigationItemInvokedAsync(IMenuItem menuItem)
|
|
|
|
|
{
|
|
|
|
|
switch (menuItem)
|
|
|
|
|
{
|
|
|
|
|
case NewMailMenuItem:
|
|
|
|
|
await NewEventAsync().ConfigureAwait(false);
|
|
|
|
|
break;
|
|
|
|
|
case ManageAccountsMenuItem:
|
|
|
|
|
NavigationService.Navigate(WinoPage.ManageAccountsPage);
|
|
|
|
|
break;
|
|
|
|
|
case SettingsItem:
|
|
|
|
|
NavigationService.Navigate(WinoPage.SettingsPage);
|
|
|
|
|
break;
|
|
|
|
|
case StoreUpdateMenuItem:
|
|
|
|
|
await StartStoreUpdateAsync().ConfigureAwait(false);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 17:46:38 +01:00
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task NewEventAsync()
|
|
|
|
|
{
|
2026-03-07 01:46:07 +01:00
|
|
|
var pickedCalendar = TryResolveConfiguredNewEventCalendar();
|
2026-03-06 17:46:38 +01:00
|
|
|
|
2026-03-07 01:46:07 +01:00
|
|
|
if (pickedCalendar == null)
|
2026-03-06 17:46:38 +01:00
|
|
|
{
|
2026-03-07 01:46:07 +01:00
|
|
|
var availableGroups = AccountCalendarStateService.GroupedAccountCalendars
|
|
|
|
|
.Where(group => group.AccountCalendars.Count > 0)
|
|
|
|
|
.Select(group => new CalendarPickerAccountGroup
|
|
|
|
|
{
|
|
|
|
|
Account = group.Account,
|
|
|
|
|
Calendars = group.AccountCalendars.Select(calendar => calendar.AccountCalendar).ToList()
|
|
|
|
|
})
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (availableGroups.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.CalendarEventCompose_NoCalendarsTitle,
|
|
|
|
|
Translator.CalendarEventCompose_NoCalendarsMessage,
|
|
|
|
|
InfoBarMessageType.Warning);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pickedCalendar = await _dialogService.ShowSingleCalendarPickerDialogAsync(availableGroups);
|
2026-03-06 17:46:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pickedCalendar == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var (startDate, endDate) = GetDefaultComposeDateRange();
|
|
|
|
|
|
|
|
|
|
NavigationService.Navigate(WinoPage.CalendarEventComposePage, new CalendarEventComposeNavigationArgs
|
|
|
|
|
{
|
|
|
|
|
SelectedCalendarId = pickedCalendar.Id,
|
|
|
|
|
StartDate = startDate,
|
|
|
|
|
EndDate = endDate
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 13:21:42 +01:00
|
|
|
public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
|
|
|
|
|
{
|
|
|
|
|
if (args.Handled || args.Mode != WinoApplicationMode.Calendar)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (args.Action == KeyboardShortcutAction.NewEvent)
|
|
|
|
|
{
|
|
|
|
|
await NewEventAsync();
|
|
|
|
|
args.Handled = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 21:55:23 +02:00
|
|
|
|
2024-12-28 16:39:43 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
[RelayCommand]
|
|
|
|
|
private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs)
|
|
|
|
|
{
|
|
|
|
|
_navigationDate = clickedDateArgs.ClickedDate;
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
ForceNavigateCalendarDate();
|
|
|
|
|
}
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
#endregion
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-12-29 14:10:09 +01:00
|
|
|
protected override void RegisterRecipients()
|
|
|
|
|
{
|
|
|
|
|
base.RegisterRecipients();
|
|
|
|
|
|
|
|
|
|
UnregisterRecipients();
|
|
|
|
|
|
|
|
|
|
Messenger.Register<VisibleDateRangeChangedMessage>(this);
|
|
|
|
|
Messenger.Register<CalendarEnableStatusChangedMessage>(this);
|
|
|
|
|
Messenger.Register<NavigateManageAccountsRequested>(this);
|
|
|
|
|
Messenger.Register<CalendarDisplayTypeChangedMessage>(this);
|
2026-02-16 01:56:22 +01:00
|
|
|
Messenger.Register<AccountRemovedMessage>(this);
|
2025-12-29 14:10:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void UnregisterRecipients()
|
|
|
|
|
{
|
|
|
|
|
base.UnregisterRecipients();
|
|
|
|
|
|
|
|
|
|
Messenger.Unregister<VisibleDateRangeChangedMessage>(this);
|
|
|
|
|
Messenger.Unregister<CalendarEnableStatusChangedMessage>(this);
|
|
|
|
|
Messenger.Unregister<NavigateManageAccountsRequested>(this);
|
|
|
|
|
Messenger.Unregister<CalendarDisplayTypeChangedMessage>(this);
|
2026-02-16 01:56:22 +01:00
|
|
|
Messenger.Unregister<AccountRemovedMessage>(this);
|
2025-12-29 14:10:09 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
public void Receive(VisibleDateRangeChangedMessage message) => HighlightedDateRange = message.DateRange;
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// Sets the header navigation items based on visible date range and calendar type.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void UpdateDateNavigationHeaderItems()
|
|
|
|
|
{
|
2026-02-13 03:09:13 +01:00
|
|
|
var settings = PreferencesService.GetCurrentCalendarSettings();
|
|
|
|
|
var cultureInfo = settings.CultureInfo ?? CultureInfo.CurrentUICulture;
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2026-02-13 03:09:13 +01:00
|
|
|
var visibleRange = HighlightedDateRange ?? new DateRange(DateTime.Today, DateTime.Today.AddDays(1));
|
|
|
|
|
var headerText = GetHeaderText(visibleRange, cultureInfo);
|
|
|
|
|
|
|
|
|
|
DateNavigationHeaderItems.ReplaceRange([headerText]);
|
|
|
|
|
SelectedDateNavigationHeaderIndex = DateNavigationHeaderItems.Count > 0 ? 0 : -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string GetHeaderText(DateRange visibleRange, CultureInfo cultureInfo)
|
|
|
|
|
{
|
|
|
|
|
var startDate = visibleRange.StartDate.Date;
|
|
|
|
|
var endDate = visibleRange.EndDate.Date > startDate ? visibleRange.EndDate.Date.AddDays(-1) : startDate;
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
switch (StatePersistenceService.CalendarDisplayType)
|
|
|
|
|
{
|
|
|
|
|
case CalendarDisplayType.Day:
|
2026-02-13 03:09:13 +01:00
|
|
|
return startDate.ToString("MMMM d, dddd", cultureInfo);
|
2025-05-18 14:06:25 +02:00
|
|
|
case CalendarDisplayType.Week:
|
|
|
|
|
case CalendarDisplayType.WorkWeek:
|
2026-02-13 03:09:13 +01:00
|
|
|
if (startDate.Month == endDate.Month && startDate.Year == endDate.Year)
|
|
|
|
|
{
|
|
|
|
|
return $"{startDate.ToString("MMMM d", cultureInfo)} - {endDate.ToString("%d", cultureInfo)}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $"{startDate.ToString("MMMM d", cultureInfo)} - {endDate.ToString("MMMM d", cultureInfo)}";
|
2025-05-18 14:06:25 +02:00
|
|
|
case CalendarDisplayType.Month:
|
2026-02-13 03:09:13 +01:00
|
|
|
return GetDominantMonthHeaderText(startDate, endDate, cultureInfo);
|
2025-05-18 14:06:25 +02:00
|
|
|
default:
|
2026-02-13 03:09:13 +01:00
|
|
|
return startDate.ToString("d", cultureInfo);
|
2024-11-10 23:28:25 +01:00
|
|
|
}
|
2025-05-18 14:06:25 +02:00
|
|
|
}
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2026-02-13 03:09:13 +01:00
|
|
|
private static string GetDominantMonthHeaderText(DateTime startDate, DateTime endDate, CultureInfo cultureInfo)
|
2025-05-18 14:06:25 +02:00
|
|
|
{
|
2026-02-13 03:09:13 +01:00
|
|
|
if (endDate < startDate)
|
|
|
|
|
{
|
|
|
|
|
endDate = startDate;
|
|
|
|
|
}
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2026-02-13 03:09:13 +01:00
|
|
|
var monthDayCounts = new Dictionary<(int Year, int Month), int>();
|
|
|
|
|
|
|
|
|
|
for (var day = startDate; day <= endDate; day = day.AddDays(1))
|
2025-05-18 14:06:25 +02:00
|
|
|
{
|
2026-02-13 03:09:13 +01:00
|
|
|
var key = (day.Year, day.Month);
|
|
|
|
|
|
|
|
|
|
if (monthDayCounts.TryGetValue(key, out var count))
|
|
|
|
|
{
|
|
|
|
|
monthDayCounts[key] = count + 1;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
monthDayCounts[key] = 1;
|
|
|
|
|
}
|
2024-11-10 23:28:25 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-13 03:09:13 +01:00
|
|
|
var dominantKey = (Year: startDate.Year, Month: startDate.Month);
|
|
|
|
|
var dominantCount = -1;
|
|
|
|
|
|
|
|
|
|
foreach (var pair in monthDayCounts)
|
|
|
|
|
{
|
|
|
|
|
if (pair.Value > dominantCount)
|
|
|
|
|
{
|
|
|
|
|
dominantCount = pair.Value;
|
|
|
|
|
dominantKey = pair.Key;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-18 14:06:25 +02:00
|
|
|
|
2026-02-13 03:09:13 +01:00
|
|
|
return new DateTime(dominantKey.Year, dominantKey.Month, 1).ToString("Y", cultureInfo);
|
2025-05-18 14:06:25 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 16:50:16 +01:00
|
|
|
partial void OnHighlightedDateRangeChanged(DateRange value)
|
|
|
|
|
{
|
|
|
|
|
UpdateDateNavigationHeaderItems();
|
|
|
|
|
}
|
2026-02-13 03:09:13 +01:00
|
|
|
|
2025-05-18 14:06:25 +02:00
|
|
|
public async void Receive(CalendarEnableStatusChangedMessage message)
|
|
|
|
|
=> await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled);
|
2024-11-10 23:28:25 +01:00
|
|
|
|
2026-03-10 16:50:16 +01:00
|
|
|
public void Receive(NavigateManageAccountsRequested message) => NavigationService.Navigate(WinoPage.ManageAccountsPage);
|
2025-01-01 17:28:29 +01:00
|
|
|
|
2026-02-13 03:09:13 +01:00
|
|
|
public void Receive(CalendarDisplayTypeChangedMessage message)
|
|
|
|
|
{
|
|
|
|
|
OnPropertyChanged(nameof(IsVerticalCalendar));
|
|
|
|
|
UpdateDateNavigationHeaderItems();
|
|
|
|
|
}
|
2026-02-16 01:56:22 +01:00
|
|
|
|
|
|
|
|
public async void Receive(AccountRemovedMessage message)
|
2026-03-07 01:46:07 +01:00
|
|
|
{
|
|
|
|
|
await InitializeAccountCalendarsAsync();
|
|
|
|
|
ValidateConfiguredNewEventCalendar();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private AccountCalendar TryResolveConfiguredNewEventCalendar()
|
|
|
|
|
{
|
|
|
|
|
ValidateConfiguredNewEventCalendar();
|
|
|
|
|
|
|
|
|
|
if (PreferencesService.NewEventButtonBehavior != NewEventButtonBehavior.AlwaysUseSpecificCalendar
|
|
|
|
|
|| !PreferencesService.DefaultNewEventCalendarId.HasValue)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return AccountCalendarStateService.AllCalendars
|
|
|
|
|
.FirstOrDefault(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value)?
|
|
|
|
|
.AccountCalendar;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ValidateConfiguredNewEventCalendar()
|
|
|
|
|
{
|
|
|
|
|
if (PreferencesService.NewEventButtonBehavior != NewEventButtonBehavior.AlwaysUseSpecificCalendar
|
|
|
|
|
|| !PreferencesService.DefaultNewEventCalendarId.HasValue)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var exists = AccountCalendarStateService.AllCalendars
|
|
|
|
|
.Any(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value);
|
|
|
|
|
|
|
|
|
|
if (exists)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime;
|
|
|
|
|
PreferencesService.DefaultNewEventCalendarId = null;
|
|
|
|
|
}
|
2026-03-06 17:46:38 +01:00
|
|
|
|
|
|
|
|
private static (DateTime StartDate, DateTime EndDate) GetDefaultComposeDateRange()
|
|
|
|
|
{
|
|
|
|
|
var localNow = DateTime.Now;
|
|
|
|
|
var roundedMinutes = localNow.Minute switch
|
|
|
|
|
{
|
|
|
|
|
< 30 => 30,
|
|
|
|
|
30 when localNow.Second == 0 && localNow.Millisecond == 0 => 30,
|
|
|
|
|
_ => 60
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var startDate = new DateTime(localNow.Year, localNow.Month, localNow.Day, localNow.Hour, 0, 0);
|
|
|
|
|
startDate = roundedMinutes == 60 ? startDate.AddHours(1) : startDate.AddMinutes(roundedMinutes);
|
|
|
|
|
|
|
|
|
|
return (startDate, startDate.AddMinutes(30));
|
|
|
|
|
}
|
2026-03-11 01:39:32 +01:00
|
|
|
|
|
|
|
|
void IShellClient.Activate(ShellModeActivationContext activationContext)
|
|
|
|
|
=> OnNavigatedTo(NavigationMode.New, activationContext);
|
|
|
|
|
|
|
|
|
|
void IShellClient.Deactivate()
|
|
|
|
|
=> OnNavigatedFrom(NavigationMode.New, null!);
|
|
|
|
|
|
|
|
|
|
Task IShellClient.HandleNavigationItemInvokedAsync(IMenuItem menuItem)
|
|
|
|
|
=> menuItem == null ? Task.CompletedTask : HandleNavigationItemInvokedAsync(menuItem);
|
|
|
|
|
|
|
|
|
|
Task IShellClient.HandleNavigationSelectionChangedAsync(IMenuItem menuItem)
|
|
|
|
|
=> Task.CompletedTask;
|
2024-11-10 23:28:25 +01:00
|
|
|
}
|
2026-03-08 11:22:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|