Fixed the display date of the calendar items. Created test project for core library, included tests for recurring calendar events.

This commit is contained in:
Burak Kaan Köse
2025-12-29 14:10:09 +01:00
parent f79305f0a6
commit 8613e92b31
33 changed files with 1168 additions and 173 deletions
@@ -64,7 +64,7 @@
<StackPanel
x:Name="AttributeStack"
Grid.Column="1"
Margin="0,4,0,0"
Margin="0,4,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Orientation="Horizontal"
@@ -1,9 +1,11 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using Itenso.TimePeriod;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Messages;
using Wino.Core.Domain;
@@ -0,0 +1,85 @@
using System.Collections.Generic;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Calendar.Controls;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Mail.WinUI.Controls.Calendar;
/// <summary>
/// AOT-Safe ItemsControl for use in UniformGrid panels.
/// </summary>
///
public partial class UniformItemsControl : Grid
{
[GeneratedDependencyProperty]
public partial DayRangeRenderModel? RenderModel { get; set; }
[GeneratedDependencyProperty]
public partial List<CalendarDayModel>? ItemsSource { get; set; }
partial void OnRenderModelChanged(DayRangeRenderModel? newValue)
{
if (newValue == null || ItemsSource == null) return;
AdjustColumns();
}
partial void OnItemsSourceChanged(List<CalendarDayModel>? newValue)
{
if (newValue == null || ItemsSource == null) return;
AdjustColumns();
}
private void AdjustColumns()
{
if (RenderModel == null || ItemsSource == null) return;
Children.Clear();
ColumnDefinitions.Clear();
var columns = RenderModel.TotalDays;
// First divide.
for (int i = 0; i < columns; i++)
{
ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
}
// Then add items.
for (int i = 0; i < columns; i++)
{
var item = ItemsSource[i];
var control = new DayColumnControl()
{
DayModel = item
};
SetColumn(control, i);
Children.Add(control);
}
}
}
//public partial class UniformItemsControl : ItemsControl
//{
// private const string ControlUniformGridName = "PART_UniformGrid";
// [GeneratedDependencyProperty]
// public partial DayRangeRenderModel? RenderModel { get; set; }
// partial void OnRenderModelChanged(DayRangeRenderModel? newValue)
// {
// if (newValue == null) return;
// // Adjust the ItemsPanel based on the RenderModel's columns.
// var uniGrid = WinoVisualTreeHelper.FindDescendants<UniformGrid>(this);
// //if (uniGrid != null)
// //{
// // uniGrid.Columns = newValue.TotalDays;
// //}
// }
//}
@@ -1,5 +1,7 @@
using System;
using System.Diagnostics;
using System.Linq;
using CommunityToolkit.WinUI;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Input;
@@ -110,6 +112,11 @@ public partial class WinoDayTimelineCanvas : Control, IDisposable
// When users click to cell we need to find the day, hour and minutes (first 30 minutes or second 30 minutes) that it represents on the timeline.
if (PositionerUIElement == null)
{
PositionerUIElement = this.FindParents().LastOrDefault(a => a is Grid);
}
PointerPoint positionerRootPoint = e.GetCurrentPoint(PositionerUIElement);
PointerPoint canvasPointerPoint = e.GetCurrentPoint(Canvas);
@@ -6,10 +6,10 @@ namespace Wino.Calendar.Selectors;
public partial class CustomAreaCalendarItemSelector : DataTemplateSelector
{
public DataTemplate AllDayTemplate { get; set; }
public DataTemplate MultiDayTemplate { get; set; }
public DataTemplate? AllDayTemplate { get; set; }
public DataTemplate? MultiDayTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container)
{
if (item is CalendarItemViewModel calendarItemViewModel)
{
@@ -5,7 +5,6 @@ using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces;
using Wino.Core.Domain.Entities.Shared;
namespace Wino.Mail.WinUI.Services;
@@ -33,16 +32,16 @@ public partial class AccountCalendarStateService : ObservableObject, IAccountCal
}
}
public IEnumerable<IGrouping<MailAccount, AccountCalendarViewModel>> GroupedAccountCalendarsEnumerable
{
get
{
return GroupedAccountCalendars
.Select(a => a.AccountCalendars)
.SelectMany(b => b)
.GroupBy(c => c.Account);
}
}
//public IEnumerable<IGrouping<MailAccount, AccountCalendarViewModel>> GroupedAccountCalendarsEnumerable
//{
// get
// {
// return GroupedAccountCalendars
// .Select(a => a.AccountCalendars)
// .SelectMany(b => b)
// .GroupBy(c => c.Account);
// }
//}
public AccountCalendarStateService()
{
+81 -47
View File
@@ -87,6 +87,9 @@ public class NavigationService : NavigationServiceBase, INavigationService
if (coreFrame == null) return false;
// Update the application mode in state persistence service
_statePersistanceService.ApplicationMode = mode;
var targetPageType = mode == WinoApplicationMode.Mail ? typeof(MailAppShell) : typeof(CalendarAppShell);
var currentPageType = coreFrame.Content?.GetType();
var transitionInfo = GetNavigationTransitionInfo(NavigationTransitionType.DrillIn);
@@ -128,73 +131,104 @@ public class NavigationService : NavigationServiceBase, INavigationService
NavigationTransitionType transition = NavigationTransitionType.None)
{
var pageType = GetPageType(page);
Frame shellFrame = GetCoreFrame(NavigationReferenceFrame.InnerShellFrame);
var currentApplicationMode = GetCoreFrame(NavigationReferenceFrame.ShellFrame)?.Content?.GetType() == typeof(MailAppShell)
? WinoApplicationMode.Mail
: WinoApplicationMode.Calendar;
_statePersistanceService.IsReadingMail = _renderingPageTypes.Contains(page);
_statePersistanceService.IsEventDetailsVisible = page == WinoPage.EventDetailsPage;
if (shellFrame != null)
Frame innerShellFrame = GetCoreFrame(NavigationReferenceFrame.InnerShellFrame);
if (innerShellFrame != null)
{
var currentFrameType = GetCurrentFrameType(ref shellFrame);
bool isCalendarShellActive = shellFrame.Content != null && shellFrame.Content.GetType() == typeof(CalendarAppShell);
if (isCalendarShellActive)
// Calendar navigations.
if (currentApplicationMode == WinoApplicationMode.Calendar)
{
return shellFrame.Navigate(pageType, parameter);
return innerShellFrame.Navigate(pageType, parameter);
}
bool isMailListingPageActive = currentFrameType != null && currentFrameType == typeof(MailListPage);
// Active page is mail list page and we are refreshing the folder.
if (isMailListingPageActive && currentFrameType == pageType && parameter is NavigateMailFolderEventArgs folderNavigationArgs)
else
{
// No need for new navigation, just refresh the folder.
WeakReferenceMessenger.Default.Send(new ActiveMailFolderChangedEvent(folderNavigationArgs.BaseFolderMenuItem, folderNavigationArgs.FolderInitLoadAwaitTask));
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
// Mail navigations.
var currentFrameType = GetCurrentFrameType(ref innerShellFrame);
bool isMailListingPageActive = currentFrameType != null && currentFrameType == typeof(MailListPage);
return true;
}
var transitionInfo = GetNavigationTransitionInfo(transition);
// This page must be opened in the Frame placed in MailListingPage.
if (isMailListingPageActive && frame == NavigationReferenceFrame.RenderingFrame)
{
var listingFrame = GetCoreFrame(NavigationReferenceFrame.RenderingFrame);
if (listingFrame == null) return false;
// Active page is mail list page and we are opening a mail item.
// No navigation needed, just refresh the rendered mail item.
if (listingFrame.Content != null
&& listingFrame.Content.GetType() == GetPageType(WinoPage.MailRenderingPage)
&& parameter is MailItemViewModel mailItemViewModel
&& page != WinoPage.ComposePage)
// Active page is mail list page and we are refreshing the folder.
if (isMailListingPageActive && currentFrameType == pageType && parameter is NavigateMailFolderEventArgs folderNavigationArgs)
{
WeakReferenceMessenger.Default.Send(new NewMailItemRenderingRequestedEvent(mailItemViewModel));
}
else if (listingFrame.Content != null
&& listingFrame.Content.GetType() == GetPageType(WinoPage.IdlePage)
&& pageType == typeof(IdlePage))
{
// Idle -> Idle navigation. Ignore.
// No need for new navigation, just refresh the folder.
WeakReferenceMessenger.Default.Send(new ActiveMailFolderChangedEvent(folderNavigationArgs.BaseFolderMenuItem, folderNavigationArgs.FolderInitLoadAwaitTask));
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
return true;
}
else
var transitionInfo = GetNavigationTransitionInfo(transition);
// This page must be opened in the Frame placed in MailListingPage.
if (isMailListingPageActive && frame == NavigationReferenceFrame.RenderingFrame)
{
listingFrame.Navigate(pageType, parameter, transitionInfo);
var listingFrame = GetCoreFrame(NavigationReferenceFrame.RenderingFrame);
if (listingFrame == null) return false;
// Active page is mail list page and we are opening a mail item.
// No navigation needed, just refresh the rendered mail item.
if (listingFrame.Content != null
&& listingFrame.Content.GetType() == GetPageType(WinoPage.MailRenderingPage)
&& parameter is MailItemViewModel mailItemViewModel
&& page != WinoPage.ComposePage)
{
WeakReferenceMessenger.Default.Send(new NewMailItemRenderingRequestedEvent(mailItemViewModel));
}
else if (listingFrame.Content != null
&& listingFrame.Content.GetType() == GetPageType(WinoPage.IdlePage)
&& pageType == typeof(IdlePage))
{
// Idle -> Idle navigation. Ignore.
return true;
}
else
{
listingFrame.Navigate(pageType, parameter, transitionInfo);
}
return true;
}
return true;
}
if ((currentFrameType != null && currentFrameType != pageType) || currentFrameType == null)
{
return shellFrame.Navigate(pageType, parameter, transitionInfo);
if ((currentFrameType != null && currentFrameType != pageType) || currentFrameType == null)
{
return innerShellFrame.Navigate(pageType, parameter, transitionInfo);
}
}
}
return false;
}
public void GoBack() => throw new NotImplementedException("GoBack method is not implemented in Wino Mail.");
public void GoBack()
{
if (_statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar)
{
var innerShellFrame = GetCoreFrame(NavigationReferenceFrame.InnerShellFrame);
if (innerShellFrame?.CanGoBack == true)
{
innerShellFrame.GoBack();
// Calendar mode: Navigate back from EventDetailsPage
_statePersistanceService.IsEventDetailsVisible = false;
}
}
else
{
// Mail mode: Clear selections and dispose rendering frame
_statePersistanceService.IsReadingMail = false;
WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested());
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
}
}
// Standalone EML viewer.
//public void NavigateRendering(MimeMessageInformation mimeMessageInformation, NavigationTransitionType transition = NavigationTransitionType.None)
@@ -9,7 +9,7 @@ namespace Wino.Services;
public class StatePersistenceService : ObservableObject, IStatePersistanceService
{
public event EventHandler<string> StatePropertyChanged;
public event EventHandler<string?>? StatePropertyChanged;
private const string OpenPaneLengthKey = nameof(OpenPaneLengthKey);
private const string MailListPaneLengthKey = nameof(MailListPaneLengthKey);
@@ -28,9 +28,40 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic
PropertyChanged += ServicePropertyChanged;
}
private void ServicePropertyChanged(object sender, PropertyChangedEventArgs e) => StatePropertyChanged?.Invoke(this, e.PropertyName);
private void ServicePropertyChanged(object? sender, PropertyChangedEventArgs e) => StatePropertyChanged?.Invoke(this, e?.PropertyName ?? string.Empty);
public bool IsBackButtonVisible => IsReadingMail && IsReaderNarrowed;
public bool IsBackButtonVisible =>
ApplicationMode == WinoApplicationMode.Mail
? IsReadingMail && IsReaderNarrowed
: IsEventDetailsVisible;
private WinoApplicationMode applicationMode = WinoApplicationMode.Mail;
public WinoApplicationMode ApplicationMode
{
get => applicationMode;
set
{
if (SetProperty(ref applicationMode, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
}
}
}
private bool isEventDetailsVisible;
public bool IsEventDetailsVisible
{
get => isEventDetailsVisible;
set
{
if (SetProperty(ref isEventDetailsVisible, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
}
}
}
private bool isReadingMail;
@@ -68,7 +99,7 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic
}
}
private string coreWindowTitle;
private string coreWindowTitle = string.Empty;
public string CoreWindowTitle
{
+1 -3
View File
@@ -9,7 +9,6 @@ using Microsoft.UI.Xaml.Controls;
using Windows.UI;
using Wino.Core.Domain.Interfaces;
using Wino.Mail.WinUI.Interfaces;
using Wino.Messaging.Client.Mails;
using Wino.Messaging.Client.Shell;
using Wino.Messaging.UI;
using Wino.Views;
@@ -114,8 +113,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient
private void BackButtonClicked(Microsoft.UI.Xaml.Controls.TitleBar sender, object args)
{
WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested());
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
NavigationService.GoBack();
}
private void MainFrameNavigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
@@ -2,6 +2,7 @@
x:Class="Wino.Styles.WinoCalendarResources"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:calendar="using:Wino.Mail.WinUI.Controls.Calendar"
xmlns:controls="using:Wino.Calendar.Controls"
xmlns:controls2="using:Wino.Mail.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -54,7 +55,6 @@
<!-- Horizontal template -->
<DataTemplate x:Key="FlipTemplate" x:DataType="models:DayRangeRenderModel">
<Grid
x:Name="RootGrid"
Background="Transparent"
ColumnSpacing="0"
RowSpacing="0">
@@ -63,6 +63,7 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- TODO: Not AOT safe. -->
<ItemsControl Margin="50,0,16,0" ItemsSource="{x:Bind CalendarDays}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:CalendarDayModel">
@@ -97,7 +98,6 @@
<controls:WinoDayTimelineCanvas
Grid.Column="1"
HalfHourSeperatorColor="{ThemeResource CalendarSeperatorBrush}"
PositionerUIElement="{Binding ElementName=RootGrid}"
RenderOptions="{x:Bind CalendarRenderOptions}"
SelectedCellBackgroundBrush="{ThemeResource CalendarFieldSelectedBackgroundBrush}"
SeperatorColor="{ThemeResource CalendarSeperatorBrush}"
@@ -112,6 +112,8 @@
ItemsSource="{x:Bind CalendarDays}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<!-- Columns="{Binding CalendarRenderOptions.TotalDayCount}" -->
<!-- TODO: Columns should come from TotalDayCount to support custom dates. -->
<toolkitControls:UniformGrid
Columns="{Binding CalendarRenderOptions.TotalDayCount}"
Orientation="Horizontal"
@@ -277,6 +279,7 @@
Margin="0,6">
<ItemsControl.ItemTemplateSelector>
<selectors:CustomAreaCalendarItemSelector>
<!-- TODO: DisplayingDate is not AOT safe. -->
<selectors:CustomAreaCalendarItemSelector.AllDayTemplate>
<DataTemplate x:DataType="data:CalendarItemViewModel">
<controls:CalendarItemControl
@@ -3,4 +3,10 @@ using Wino.Mail.WinUI;
namespace Wino.Calendar.Views.Abstract;
public abstract class CalendarPageAbstract : BasePage<CalendarPageViewModel> { }
public abstract class CalendarPageAbstract : BasePage<CalendarPageViewModel>
{
protected CalendarPageAbstract()
{
NavigationCacheMode = Microsoft.UI.Xaml.Navigation.NavigationCacheMode.Enabled;
}
}
@@ -156,6 +156,7 @@
<calendarControls:WinoCalendarView
x:Name="CalendarView"
Grid.Row="0"
Margin="0,12,0,0"
HorizontalAlignment="Center"
DateClickedCommand="{x:Bind ViewModel.DateClickedCommand}"
HighlightedDateRange="{x:Bind ViewModel.HighlightedDateRange, Mode=OneWay}"
@@ -18,9 +18,9 @@
<Page.Resources>
<CollectionViewSource
x:Key="GroupedCalendarEnumerableViewSource"
x:Name="GroupedCalendarEnumerableViewSource"
IsSourceGrouped="True"
Source="{x:Bind ViewModel.AccountCalendarStateService.GroupedAccountCalendarsEnumerable, Mode=OneWay}" />
Source="{x:Bind ViewModel.AccountCalendarStateService.GroupedAccountCalendars, Mode=OneWay}" />
</Page.Resources>
<Border
@@ -124,7 +124,7 @@
<ListView
MaxHeight="300"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Source={StaticResource GroupedCalendarEnumerableViewSource}}"
ItemsSource="{x:Bind GroupedCalendarEnumerableViewSource.View, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedQuickEventAccountCalendar, Mode=TwoWay}"
SelectionChanged="QuickEventAccountSelectorSelectionChanged">
<ListView.ItemTemplate>
@@ -22,7 +22,6 @@ public sealed partial class CalendarPage : CalendarPageAbstract,
public CalendarPage()
{
InitializeComponent();
NavigationCacheMode = NavigationCacheMode.Enabled;
ViewModel.DetailsShowCalendarItemChanged += CalendarItemDetailContextChanged;
}
@@ -40,6 +39,26 @@ public sealed partial class CalendarPage : CalendarPageAbstract,
}
}
protected override void RegisterRecipients()
{
base.RegisterRecipients();
WeakReferenceMessenger.Default.Register<ScrollToDateMessage>(this);
WeakReferenceMessenger.Default.Register<ScrollToHourMessage>(this);
WeakReferenceMessenger.Default.Register<GoNextDateRequestedMessage>(this);
WeakReferenceMessenger.Default.Register<GoPreviousDateRequestedMessage>(this);
}
protected override void UnregisterRecipients()
{
base.UnregisterRecipients();
WeakReferenceMessenger.Default.Unregister<ScrollToDateMessage>(this);
WeakReferenceMessenger.Default.Unregister<ScrollToHourMessage>(this);
WeakReferenceMessenger.Default.Unregister<GoNextDateRequestedMessage>(this);
WeakReferenceMessenger.Default.Unregister<GoPreviousDateRequestedMessage>(this);
}
public void Receive(ScrollToHourMessage message) => CalendarControl.NavigateToHour(message.TimeSpan);
public void Receive(ScrollToDateMessage message) => CalendarControl.NavigateToDay(message.Date);
public void Receive(GoNextDateRequestedMessage message) => CalendarControl.GoNextRange();