2026-03-21 00:58:01 +01:00
|
|
|
using System;
|
2026-03-25 09:45:49 +01:00
|
|
|
using System.Collections.ObjectModel;
|
|
|
|
|
using System.ComponentModel;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
2025-12-26 20:46:48 +01:00
|
|
|
using CommunityToolkit.Mvvm.Messaging;
|
2026-03-25 09:45:49 +01:00
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2026-03-23 14:56:36 +01:00
|
|
|
using Microsoft.UI.Xaml;
|
|
|
|
|
using Microsoft.UI.Xaml.Controls;
|
|
|
|
|
using Microsoft.UI.Xaml.Controls.Primitives;
|
2025-12-26 20:46:48 +01:00
|
|
|
using Microsoft.UI.Xaml.Navigation;
|
2026-03-23 14:56:36 +01:00
|
|
|
using Windows.Foundation;
|
2026-03-25 09:45:49 +01:00
|
|
|
using Wino.Core.Domain;
|
2026-03-25 15:49:14 +01:00
|
|
|
using Wino.Core.Domain.Enums;
|
2026-03-23 14:56:36 +01:00
|
|
|
using Wino.Calendar.Controls;
|
2025-12-26 20:46:48 +01:00
|
|
|
using Wino.Calendar.Views.Abstract;
|
2026-03-25 09:45:49 +01:00
|
|
|
using Wino.Core.Domain.Entities.Calendar;
|
2025-12-26 20:46:48 +01:00
|
|
|
using Wino.Core.Domain.Models.Calendar;
|
2026-03-25 09:45:49 +01:00
|
|
|
using Wino.Core.Domain.Interfaces;
|
|
|
|
|
using Wino.Mail.WinUI;
|
|
|
|
|
using Wino.Mail.WinUI.Interfaces;
|
|
|
|
|
using Wino.Mail.WinUI.Models;
|
2025-12-26 20:46:48 +01:00
|
|
|
using Wino.Messaging.Client.Calendar;
|
|
|
|
|
|
|
|
|
|
namespace Wino.Calendar.Views;
|
|
|
|
|
|
2026-03-25 09:45:49 +01:00
|
|
|
public sealed partial class CalendarPage : CalendarPageAbstract, ITitleBarSearchHost
|
2025-12-26 20:46:48 +01:00
|
|
|
{
|
2026-03-23 14:56:36 +01:00
|
|
|
private const int PopupDialogOffset = 12;
|
2026-03-25 09:45:49 +01:00
|
|
|
private ICalendarShellClient CalendarShellClient { get; } = WinoApplication.Current.Services.GetRequiredService<ICalendarShellClient>();
|
|
|
|
|
private CancellationTokenSource? _searchCancellationTokenSource;
|
|
|
|
|
private long _calendarTypeSelectorChangedToken;
|
2026-03-25 15:49:14 +01:00
|
|
|
private bool _suppressSelectionResetOnPopupClose;
|
2026-03-25 09:45:49 +01:00
|
|
|
|
|
|
|
|
public ObservableCollection<TitleBarSearchSuggestion> SearchSuggestions { get; } = [];
|
|
|
|
|
|
|
|
|
|
public string SearchText { get; set; } = string.Empty;
|
|
|
|
|
|
|
|
|
|
public string SearchPlaceholderText => Translator.SearchBarPlaceholder;
|
2026-03-23 14:56:36 +01:00
|
|
|
|
2025-12-26 20:46:48 +01:00
|
|
|
public CalendarPage()
|
|
|
|
|
{
|
|
|
|
|
InitializeComponent();
|
2026-03-25 09:45:49 +01:00
|
|
|
_calendarTypeSelectorChangedToken = CalendarToolbar.RegisterSelectedTypeChanged(CalendarTypeSelectorSelectedTypeChanged);
|
|
|
|
|
CalendarToolbar.PreviousDateRequested += CalendarToolbarPreviousDateRequested;
|
|
|
|
|
CalendarToolbar.NextDateRequested += CalendarToolbarNextDateRequested;
|
|
|
|
|
ViewModel.PropertyChanged += ViewModelPropertyChanged;
|
|
|
|
|
CalendarShellClient.PropertyChanged += CalendarShellClientPropertyChanged;
|
|
|
|
|
CalendarShellClient.StatePersistenceService.StatePropertyChanged += CalendarStatePersistenceServiceChanged;
|
|
|
|
|
Unloaded += CalendarPageUnloaded;
|
|
|
|
|
RefreshCalendarToolbar();
|
2025-12-26 20:46:48 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-11 19:26:37 +01:00
|
|
|
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
|
|
|
|
{
|
2026-03-25 15:49:14 +01:00
|
|
|
CloseQuickEventPopup(clearSelection: true);
|
2026-03-11 19:26:37 +01:00
|
|
|
base.OnNavigatingFrom(e);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 20:46:48 +01:00
|
|
|
protected override void OnNavigatedTo(NavigationEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
base.OnNavigatedTo(e);
|
2026-03-25 09:45:49 +01:00
|
|
|
RefreshCalendarToolbar();
|
2025-12-26 20:46:48 +01:00
|
|
|
|
2026-03-21 00:58:01 +01:00
|
|
|
if (e.NavigationMode == NavigationMode.Back && ViewModel.RestoreVisibleState())
|
2026-03-11 19:26:37 +01:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-26 20:46:48 +01:00
|
|
|
|
2026-03-21 00:58:01 +01:00
|
|
|
var anchorDate = DateOnly.FromDateTime(DateTime.Now.Date);
|
2025-12-26 20:46:48 +01:00
|
|
|
|
2026-03-21 00:58:01 +01:00
|
|
|
if (e.Parameter is CalendarPageNavigationArgs args && !args.RequestDefaultNavigation)
|
2025-12-26 20:46:48 +01:00
|
|
|
{
|
2026-03-21 00:58:01 +01:00
|
|
|
anchorDate = DateOnly.FromDateTime(args.NavigationDate.Date);
|
2025-12-26 20:46:48 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-21 00:58:01 +01:00
|
|
|
var request = new CalendarDisplayRequest(ViewModel.StatePersistanceService.CalendarDisplayType, anchorDate);
|
|
|
|
|
WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(request));
|
2025-12-26 20:46:48 +01:00
|
|
|
}
|
2026-03-23 14:56:36 +01:00
|
|
|
|
2026-03-25 09:45:49 +01:00
|
|
|
public async Task OnTitleBarSearchTextChangedAsync()
|
|
|
|
|
{
|
|
|
|
|
_searchCancellationTokenSource?.Cancel();
|
|
|
|
|
_searchCancellationTokenSource?.Dispose();
|
|
|
|
|
_searchCancellationTokenSource = null;
|
|
|
|
|
|
|
|
|
|
SearchSuggestions.Clear();
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(SearchText))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
_searchCancellationTokenSource = new CancellationTokenSource();
|
|
|
|
|
var cancellationToken = _searchCancellationTokenSource.Token;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await Task.Delay(150, cancellationToken);
|
|
|
|
|
var results = await ViewModel.SearchCalendarItemsAsync(SearchText, 6, cancellationToken);
|
|
|
|
|
|
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
foreach (var result in results)
|
|
|
|
|
{
|
|
|
|
|
var subtitleParts = new[]
|
|
|
|
|
{
|
|
|
|
|
result.AssignedCalendar?.MailAccount?.Name,
|
|
|
|
|
result.AssignedCalendar?.Name,
|
|
|
|
|
result.LocalStartDate.ToString("g")
|
|
|
|
|
}.Where(part => !string.IsNullOrWhiteSpace(part));
|
|
|
|
|
|
|
|
|
|
SearchSuggestions.Add(new TitleBarSearchSuggestion(result.Title, string.Join(" • ", subtitleParts), result));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnTitleBarSearchSuggestionChosen(TitleBarSearchSuggestion suggestion)
|
|
|
|
|
{
|
|
|
|
|
SearchText = suggestion.Title;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task OnTitleBarSearchSubmittedAsync(string queryText, TitleBarSearchSuggestion? chosenSuggestion)
|
|
|
|
|
{
|
|
|
|
|
SearchText = queryText;
|
|
|
|
|
|
|
|
|
|
if (chosenSuggestion?.Tag is CalendarItem selectedItem)
|
|
|
|
|
{
|
|
|
|
|
ViewModel.OpenCalendarSearchResult(selectedItem);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = (await ViewModel.SearchCalendarItemsAsync(queryText, 1, CancellationToken.None)).FirstOrDefault();
|
|
|
|
|
if (result != null)
|
|
|
|
|
{
|
|
|
|
|
ViewModel.OpenCalendarSearchResult(result);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 14:56:36 +01:00
|
|
|
private void CalendarSurfaceEmptySlotTapped(object sender, CalendarEmptySlotTappedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (ViewModel.DisplayDetailsCalendarItemViewModel != null)
|
|
|
|
|
{
|
|
|
|
|
ViewModel.DisplayDetailsCalendarItemViewModel = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ViewModel.SelectedQuickEventDate = e.ClickedDate;
|
2026-03-25 15:49:14 +01:00
|
|
|
ViewModel.IsAllDay = ViewModel.CurrentVisibleRange?.DisplayType == CalendarDisplayType.Month;
|
2026-03-23 14:56:36 +01:00
|
|
|
|
|
|
|
|
var transform = CalendarSurface.TransformToVisual(CalendarOverlayCanvas);
|
2026-03-25 15:49:14 +01:00
|
|
|
var canvasPoint = transform.TransformPoint(e.AnchorPoint);
|
2026-03-23 14:56:36 +01:00
|
|
|
|
2026-03-25 15:49:14 +01:00
|
|
|
TeachingTipPositionerGrid.Width = 1;
|
|
|
|
|
TeachingTipPositionerGrid.Height = 1;
|
2026-03-23 14:56:36 +01:00
|
|
|
|
|
|
|
|
Canvas.SetLeft(TeachingTipPositionerGrid, canvasPoint.X);
|
|
|
|
|
Canvas.SetTop(TeachingTipPositionerGrid, canvasPoint.Y);
|
|
|
|
|
|
2026-03-25 15:49:14 +01:00
|
|
|
if (!ViewModel.IsAllDay)
|
|
|
|
|
{
|
|
|
|
|
var startTime = e.ClickedDate.TimeOfDay;
|
|
|
|
|
var endTime = startTime.Add(TimeSpan.FromMinutes(30));
|
|
|
|
|
ViewModel.SelectQuickEventTimeRange(startTime, endTime);
|
|
|
|
|
}
|
2026-03-23 14:56:36 +01:00
|
|
|
|
2026-03-25 15:49:14 +01:00
|
|
|
_suppressSelectionResetOnPopupClose = true;
|
|
|
|
|
QuickEventPopupDialog.IsOpen = false;
|
2026-03-23 14:56:36 +01:00
|
|
|
QuickEventPopupDialog.IsOpen = true;
|
2026-03-25 15:49:14 +01:00
|
|
|
_suppressSelectionResetOnPopupClose = false;
|
2026-03-23 14:56:36 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 23:46:02 +02:00
|
|
|
private async void CalendarSurfaceCalendarItemDropped(object sender, CalendarItemDroppedEventArgs e)
|
|
|
|
|
=> await ViewModel.MoveCalendarItemAsync(e.CalendarItemViewModel, e.TargetStart);
|
|
|
|
|
|
2026-03-23 14:56:36 +01:00
|
|
|
private void QuickEventAccountSelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
|
|
|
=> QuickEventAccountSelectorFlyout.Hide();
|
|
|
|
|
|
|
|
|
|
private void QuickEventPopupClosed(object sender, object e)
|
|
|
|
|
{
|
2026-03-25 15:49:14 +01:00
|
|
|
if (!_suppressSelectionResetOnPopupClose)
|
|
|
|
|
{
|
|
|
|
|
ViewModel.SelectedQuickEventDate = null;
|
|
|
|
|
}
|
2026-03-23 14:56:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void PopupPlacementChanged(object sender, object e)
|
|
|
|
|
{
|
|
|
|
|
if (sender is not Popup popup)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
popup.HorizontalOffset = 0;
|
|
|
|
|
popup.VerticalOffset = 0;
|
|
|
|
|
|
|
|
|
|
switch (popup.ActualPlacement)
|
|
|
|
|
{
|
|
|
|
|
case PopupPlacementMode.Top:
|
|
|
|
|
popup.VerticalOffset = PopupDialogOffset * -1;
|
|
|
|
|
break;
|
|
|
|
|
case PopupPlacementMode.Bottom:
|
|
|
|
|
popup.VerticalOffset = PopupDialogOffset;
|
|
|
|
|
break;
|
|
|
|
|
case PopupPlacementMode.Left:
|
|
|
|
|
popup.HorizontalOffset = PopupDialogOffset * -1;
|
|
|
|
|
break;
|
|
|
|
|
case PopupPlacementMode.Right:
|
|
|
|
|
popup.HorizontalOffset = PopupDialogOffset;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void StartTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
|
|
|
|
|
=> ViewModel.SelectedStartTimeString = args.Text;
|
|
|
|
|
|
|
|
|
|
private void EndTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
|
|
|
|
|
=> ViewModel.SelectedEndTimeString = args.Text;
|
2026-03-25 09:45:49 +01:00
|
|
|
|
|
|
|
|
private void CalendarTypeSelectorSelectedTypeChanged(DependencyObject sender, DependencyProperty dp)
|
|
|
|
|
{
|
|
|
|
|
var selectedType = CalendarToolbar.SelectedType;
|
|
|
|
|
if (CalendarShellClient.StatePersistenceService.CalendarDisplayType != selectedType)
|
|
|
|
|
{
|
|
|
|
|
CalendarShellClient.StatePersistenceService.CalendarDisplayType = selectedType;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CalendarToolbarPreviousDateRequested(object? sender, EventArgs e)
|
|
|
|
|
=> CalendarShellClient.PreviousDateRangeCommand.Execute(null);
|
|
|
|
|
|
|
|
|
|
private void CalendarToolbarNextDateRequested(object? sender, EventArgs e)
|
|
|
|
|
=> CalendarShellClient.NextDateRangeCommand.Execute(null);
|
|
|
|
|
|
|
|
|
|
private void ViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (e.PropertyName == nameof(ViewModel.VisibleDateRangeText))
|
|
|
|
|
{
|
|
|
|
|
RefreshCalendarToolbar();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CalendarShellClientPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (e.PropertyName == nameof(ICalendarShellClient.VisibleDateRangeText))
|
|
|
|
|
{
|
|
|
|
|
RefreshCalendarToolbar();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CalendarStatePersistenceServiceChanged(object? sender, string propertyName)
|
|
|
|
|
{
|
|
|
|
|
if (propertyName == nameof(IStatePersistanceService.CalendarDisplayType) ||
|
|
|
|
|
propertyName == nameof(IStatePersistanceService.DayDisplayCount))
|
|
|
|
|
{
|
|
|
|
|
RefreshCalendarToolbar();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RefreshCalendarToolbar()
|
|
|
|
|
{
|
|
|
|
|
CalendarToolbar.VisibleDateRangeText = CalendarShellClient.VisibleDateRangeText;
|
|
|
|
|
CalendarToolbar.TodayClickedCommand = CalendarShellClient.TodayClickedCommand;
|
|
|
|
|
CalendarToolbar.DisplayDayCount = CalendarShellClient.StatePersistenceService.DayDisplayCount;
|
|
|
|
|
|
|
|
|
|
if (CalendarToolbar.SelectedType != CalendarShellClient.StatePersistenceService.CalendarDisplayType)
|
|
|
|
|
{
|
|
|
|
|
CalendarToolbar.SelectedType = CalendarShellClient.StatePersistenceService.CalendarDisplayType;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CalendarPageUnloaded(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
2026-03-25 15:49:14 +01:00
|
|
|
CloseQuickEventPopup(clearSelection: true);
|
2026-03-25 09:45:49 +01:00
|
|
|
CalendarToolbar.UnregisterSelectedTypeChanged(_calendarTypeSelectorChangedToken);
|
|
|
|
|
CalendarToolbar.PreviousDateRequested -= CalendarToolbarPreviousDateRequested;
|
|
|
|
|
CalendarToolbar.NextDateRequested -= CalendarToolbarNextDateRequested;
|
|
|
|
|
ViewModel.PropertyChanged -= ViewModelPropertyChanged;
|
|
|
|
|
CalendarShellClient.PropertyChanged -= CalendarShellClientPropertyChanged;
|
|
|
|
|
CalendarShellClient.StatePersistenceService.StatePropertyChanged -= CalendarStatePersistenceServiceChanged;
|
|
|
|
|
_searchCancellationTokenSource?.Cancel();
|
|
|
|
|
_searchCancellationTokenSource?.Dispose();
|
|
|
|
|
Unloaded -= CalendarPageUnloaded;
|
|
|
|
|
}
|
2026-03-25 15:49:14 +01:00
|
|
|
|
|
|
|
|
private async void SaveQuickEventClicked(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (!ViewModel.SaveQuickEventCommand.CanExecute(null))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_suppressSelectionResetOnPopupClose = true;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
QuickEventPopupDialog.IsOpen = false;
|
|
|
|
|
await ViewModel.SaveQuickEventCommand.ExecuteAsync(null);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_suppressSelectionResetOnPopupClose = false;
|
|
|
|
|
ViewModel.SelectedQuickEventDate = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void MoreDetailsClicked(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (!ViewModel.GoToEventComposePageCommand.CanExecute(null))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_suppressSelectionResetOnPopupClose = true;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
QuickEventPopupDialog.IsOpen = false;
|
|
|
|
|
ViewModel.GoToEventComposePageCommand.Execute(null);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_suppressSelectionResetOnPopupClose = false;
|
|
|
|
|
ViewModel.SelectedQuickEventDate = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CloseQuickEventPopup(bool clearSelection)
|
|
|
|
|
{
|
|
|
|
|
_suppressSelectionResetOnPopupClose = !clearSelection;
|
|
|
|
|
QuickEventPopupDialog.IsOpen = false;
|
|
|
|
|
_suppressSelectionResetOnPopupClose = false;
|
|
|
|
|
|
|
|
|
|
if (clearSelection)
|
|
|
|
|
{
|
|
|
|
|
ViewModel.SelectedQuickEventDate = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-26 20:46:48 +01:00
|
|
|
}
|