diff --git a/Directory.Packages.props b/Directory.Packages.props
index ebc19048..9eb02f44 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -7,13 +7,13 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -23,22 +23,22 @@
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
+
+
@@ -46,24 +46,24 @@
-
+
-
+
-
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
diff --git a/Wino.Calendar.ViewModels/AccountDetailsPageViewModel.cs b/Wino.Calendar.ViewModels/AccountDetailsPageViewModel.cs
deleted file mode 100644
index a110d386..00000000
--- a/Wino.Calendar.ViewModels/AccountDetailsPageViewModel.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using CommunityToolkit.Mvvm.Input;
-using CommunityToolkit.Mvvm.Messaging;
-using Wino.Calendar.ViewModels.Interfaces;
-using Wino.Core.Domain;
-using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.Domain.Models.Navigation;
-using Wino.Core.ViewModels;
-using Wino.Mail.ViewModels.Data;
-using Wino.Messaging.Client.Navigation;
-
-namespace Wino.Calendar.ViewModels;
-
-public partial class AccountDetailsPageViewModel : CalendarBaseViewModel
-{
- private readonly IAccountService _accountService;
-
- public AccountProviderDetailViewModel Account { get; private set; }
- public ICalendarDialogService CalendarDialogService { get; }
- public IAccountCalendarStateService AccountCalendarStateService { get; }
-
- public AccountDetailsPageViewModel(ICalendarDialogService calendarDialogService, IAccountService accountService, IAccountCalendarStateService accountCalendarStateService)
- {
- CalendarDialogService = calendarDialogService;
- _accountService = accountService;
- AccountCalendarStateService = accountCalendarStateService;
- }
-
- [RelayCommand]
- private void EditAccountDetails()
- => Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsEditAccountDetails_Title, WinoPage.EditAccountDetailsPage, Account));
-
- public override void OnNavigatedTo(NavigationMode mode, object parameters)
- {
- base.OnNavigatedTo(mode, parameters);
- }
-}
diff --git a/Wino.Calendar.ViewModels/AccountManagementViewModel.cs b/Wino.Calendar.ViewModels/AccountManagementViewModel.cs
deleted file mode 100644
index f7eb9563..00000000
--- a/Wino.Calendar.ViewModels/AccountManagementViewModel.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using CommunityToolkit.Mvvm.Input;
-using Wino.Core.Domain;
-using Wino.Core.Domain.Entities.Shared;
-using Wino.Core.Domain.Enums;
-using Wino.Core.Domain.Exceptions;
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.Domain.Models.Authentication;
-using Wino.Core.Domain.Models.Navigation;
-using Wino.Core.Domain.Models.Synchronization;
-using Wino.Core.ViewModels;
-using Wino.Messaging.Server;
-
-namespace Wino.Calendar.ViewModels;
-
-public partial class AccountManagementViewModel : AccountManagementPageViewModelBase
-{
- private readonly IProviderService _providerService;
-
- public AccountManagementViewModel(ICalendarDialogService dialogService,
- IWinoServerConnectionManager winoServerConnectionManager,
- INavigationService navigationService,
- IAccountService accountService,
- IProviderService providerService,
- IStoreManagementService storeManagementService,
- IAuthenticationProvider authenticationProvider,
- IPreferencesService preferencesService) : base(dialogService, winoServerConnectionManager, navigationService, accountService, providerService, storeManagementService, authenticationProvider, preferencesService)
- {
- CalendarDialogService = dialogService;
- _providerService = providerService;
- }
-
- public ICalendarDialogService CalendarDialogService { get; }
-
- public override async void OnNavigatedTo(NavigationMode mode, object parameters)
- {
- base.OnNavigatedTo(mode, parameters);
-
- await InitializeAccountsAsync();
- }
-
- public override async Task InitializeAccountsAsync()
- {
- Accounts.Clear();
-
- var accounts = await AccountService.GetAccountsAsync().ConfigureAwait(false);
-
- await ExecuteUIThread(() =>
- {
- foreach (var account in accounts)
- {
- var accountDetails = GetAccountProviderDetails(account);
-
- Accounts.Add(accountDetails);
- }
- });
-
- await ManageStorePurchasesAsync().ConfigureAwait(false);
- }
-
- [RelayCommand]
- private async Task AddNewAccountAsync()
- {
- if (IsAccountCreationBlocked)
- {
- var isPurchaseClicked = await DialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_AccountLimitMessage, Translator.DialogMessage_AccountLimitTitle, Translator.Buttons_Purchase);
-
- if (!isPurchaseClicked) return;
-
- await PurchaseUnlimitedAccountAsync();
-
- return;
- }
-
- var availableProviders = _providerService.GetAvailableProviders();
-
- var accountCreationDialogResult = await DialogService.ShowAccountProviderSelectionDialogAsync(availableProviders);
-
- if (accountCreationDialogResult == null) return;
-
- var accountCreationCancellationTokenSource = new CancellationTokenSource();
- var accountCreationDialog = CalendarDialogService.GetAccountCreationDialog(accountCreationDialogResult);
-
- await accountCreationDialog.ShowDialogAsync(accountCreationCancellationTokenSource);
- await Task.Delay(500);
-
- // For OAuth authentications, we just generate token and assign it to the MailAccount.
-
- var createdAccount = new MailAccount()
- {
- ProviderType = accountCreationDialogResult.ProviderType,
- Name = accountCreationDialogResult.AccountName,
- Id = Guid.NewGuid()
- };
-
- var tokenInformationResponse = await WinoServerConnectionManager
- .GetResponseAsync(new AuthorizationRequested(accountCreationDialogResult.ProviderType,
- createdAccount,
- createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token);
-
- if (accountCreationDialog.State == AccountCreationDialogState.Canceled)
- throw new AccountSetupCanceledException();
-
- tokenInformationResponse.ThrowIfFailed();
-
- await AccountService.CreateAccountAsync(createdAccount, null);
-
- // Sync profile information if supported.
- if (createdAccount.IsProfileInfoSyncSupported)
- {
- // Start profile information synchronization.
- // It's only available for Outlook and Gmail synchronizers.
-
- var profileSyncOptions = new MailSynchronizationOptions()
- {
- AccountId = createdAccount.Id,
- Type = MailSynchronizationType.UpdateProfile
- };
-
- var profileSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync(new NewMailSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client));
-
- var profileSynchronizationResult = profileSynchronizationResponse.Data;
-
- if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
- throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
-
- createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName;
- createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
-
- await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
- }
-
- accountCreationDialog.State = AccountCreationDialogState.FetchingEvents;
-
- // Start synchronizing events.
- var synchronizationOptions = new CalendarSynchronizationOptions()
- {
- AccountId = createdAccount.Id,
- Type = CalendarSynchronizationType.CalendarMetadata
- };
-
- var synchronizationResponse = await WinoServerConnectionManager.GetResponseAsync(new NewCalendarSynchronizationRequested(synchronizationOptions, SynchronizationSource.Client));
- }
-}
diff --git a/Wino.Calendar.ViewModels/AppShellViewModel.cs b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs
similarity index 98%
rename from Wino.Calendar.ViewModels/AppShellViewModel.cs
rename to Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs
index d6529511..8f656180 100644
--- a/Wino.Calendar.ViewModels/AppShellViewModel.cs
+++ b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs
@@ -23,7 +23,7 @@ using Wino.Messaging.Server;
namespace Wino.Calendar.ViewModels;
-public partial class AppShellViewModel : CalendarBaseViewModel,
+public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
IRecipient,
IRecipient,
IRecipient,
@@ -69,7 +69,7 @@ public partial class AppShellViewModel : CalendarBaseViewModel,
// For updating account calendars asynchronously.
private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
- public AppShellViewModel(IPreferencesService preferencesService,
+ public CalendarAppShellViewModel(IPreferencesService preferencesService,
IStatePersistanceService statePersistanceService,
IAccountService accountService,
ICalendarService calendarService,
@@ -223,7 +223,7 @@ public partial class AppShellViewModel : CalendarBaseViewModel,
{
AccountId = account.Id,
Type = CalendarSynchronizationType.CalendarMetadata
- }, SynchronizationSource.Client);
+ });
Messenger.Send(t);
}
diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs
index 99cd7a94..22012b17 100644
--- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs
+++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs
@@ -711,7 +711,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
}
}
- partial void OnSelectedStartTimeStringChanged(string newValue)
+ partial void OnSelectedStartTimeStringChanged(string oldValue, string newValue)
{
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
@@ -725,7 +725,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
}
}
- partial void OnSelectedEndTimeStringChanged(string newValue)
+ partial void OnSelectedEndTimeStringChanged(string oldValue, string newValue)
{
var parsedTime = CurrentSettings.GetTimeSpan(newValue);
diff --git a/Wino.Calendar.ViewModels/CalendarViewModelContainerSetup.cs b/Wino.Calendar.ViewModels/CalendarViewModelContainerSetup.cs
deleted file mode 100644
index 2f8b452d..00000000
--- a/Wino.Calendar.ViewModels/CalendarViewModelContainerSetup.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Wino.Core;
-
-namespace Wino.Calendar.ViewModels;
-
-public static class CalendarViewModelContainerSetup
-{
- public static void RegisterCalendarViewModelServices(this IServiceCollection services)
- {
- services.RegisterCoreServices();
- }
-}
diff --git a/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs b/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs
index 92b388ba..28b05f6a 100644
--- a/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs
+++ b/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs
@@ -98,7 +98,7 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
_isExternalPropChangeBlocked = false;
}
- partial void OnIsCheckedStateChanged(bool? newValue)
+ partial void OnIsCheckedStateChanged(bool? oldValue, bool? newValue)
{
if (_isExternalPropChangeBlocked) return;
diff --git a/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj b/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj
index ddc81fa8..c1a3c878 100644
--- a/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj
+++ b/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj
@@ -1,6 +1,6 @@
- net9.0
+ net10.0
x86;x64;arm64
win-x86;win-x64;win-arm64
true
diff --git a/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs b/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs
index de03e74e..4baba8cd 100644
--- a/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs
+++ b/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs
@@ -30,6 +30,18 @@ public class CalendarItem : ICalendarItem
public TimeSpan StartDateOffset { get; set; }
public TimeSpan EndDateOffset { get; set; }
+ ///
+ /// IANA timezone identifier for the start time (e.g., "America/New_York", "Europe/London").
+ /// If null or empty, UTC is assumed.
+ ///
+ public string StartTimeZone { get; set; }
+
+ ///
+ /// IANA timezone identifier for the end time (e.g., "America/New_York", "Europe/London").
+ /// If null or empty, UTC is assumed.
+ ///
+ public string EndTimeZone { get; set; }
+
private ITimePeriod _period;
public ITimePeriod Period
{
@@ -170,10 +182,70 @@ public class CalendarItem : ICalendarItem
HtmlLink = HtmlLink,
StartDateOffset = StartDateOffset,
EndDateOffset = EndDateOffset,
+ StartTimeZone = StartTimeZone,
+ EndTimeZone = EndTimeZone,
RemoteEventId = RemoteEventId,
IsHidden = IsHidden,
IsLocked = IsLocked,
IsOccurrence = true
};
}
+
+ ///
+ /// Gets the start date as a DateTimeOffset with the correct timezone.
+ /// If StartTimeZone is available, uses it to calculate the offset.
+ /// Otherwise, uses the stored StartDateOffset or assumes UTC.
+ ///
+ [Ignore]
+ public DateTimeOffset StartDateTimeOffset
+ {
+ get
+ {
+ if (!string.IsNullOrEmpty(StartTimeZone))
+ {
+ try
+ {
+ var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(StartTimeZone);
+ var offset = timeZoneInfo.GetUtcOffset(StartDate);
+ return new DateTimeOffset(StartDate, offset);
+ }
+ catch
+ {
+ // If timezone lookup fails, fall back to stored offset
+ }
+ }
+
+ // Fall back to stored offset, or UTC if offset is zero
+ return new DateTimeOffset(StartDate, StartDateOffset);
+ }
+ }
+
+ ///
+ /// Gets the end date as a DateTimeOffset with the correct timezone.
+ /// If EndTimeZone is available, uses it to calculate the offset.
+ /// Otherwise, uses the stored EndDateOffset or assumes UTC.
+ ///
+ [Ignore]
+ public DateTimeOffset EndDateTimeOffset
+ {
+ get
+ {
+ if (!string.IsNullOrEmpty(EndTimeZone))
+ {
+ try
+ {
+ var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(EndTimeZone);
+ var offset = timeZoneInfo.GetUtcOffset(EndDate);
+ return new DateTimeOffset(EndDate, offset);
+ }
+ catch
+ {
+ // If timezone lookup fails, fall back to stored offset
+ }
+ }
+
+ // Fall back to stored offset, or UTC if offset is zero
+ return new DateTimeOffset(EndDate, EndDateOffset);
+ }
+ }
}
diff --git a/Wino.Core.Domain/Enums/NavigationReferenceFrame.cs b/Wino.Core.Domain/Enums/NavigationReferenceFrame.cs
index 3495a98e..e7279206 100644
--- a/Wino.Core.Domain/Enums/NavigationReferenceFrame.cs
+++ b/Wino.Core.Domain/Enums/NavigationReferenceFrame.cs
@@ -3,5 +3,6 @@
public enum NavigationReferenceFrame
{
ShellFrame,
+ InnerShellFrame,
RenderingFrame
}
diff --git a/Wino.Core.Domain/Enums/WinoApplicationMode.cs b/Wino.Core.Domain/Enums/WinoApplicationMode.cs
new file mode 100644
index 00000000..3c51ece4
--- /dev/null
+++ b/Wino.Core.Domain/Enums/WinoApplicationMode.cs
@@ -0,0 +1,7 @@
+namespace Wino.Core.Domain.Enums;
+
+public enum WinoApplicationMode
+{
+ Mail,
+ Calendar
+}
diff --git a/Wino.Core.Domain/Interfaces/ISynchronizationManager.cs b/Wino.Core.Domain/Interfaces/ISynchronizationManager.cs
index 65df1c23..61017244 100644
--- a/Wino.Core.Domain/Interfaces/ISynchronizationManager.cs
+++ b/Wino.Core.Domain/Interfaces/ISynchronizationManager.cs
@@ -64,6 +64,12 @@ public interface ISynchronizationManager
Task SynchronizeProfileAsync(Guid accountId,
CancellationToken cancellationToken = default);
+ ///
+ /// Handles calendar synchronization for the given account.
+ ///
+ Task SynchronizeCalendarAsync(CalendarSynchronizationOptions options,
+ CancellationToken cancellationToken = default);
+
///
/// Downloads a MIME message for the given mail item.
///
diff --git a/Wino.Core.Domain/Interfaces/IWinoNavigationService.cs b/Wino.Core.Domain/Interfaces/IWinoNavigationService.cs
index 28b015c7..b343dd00 100644
--- a/Wino.Core.Domain/Interfaces/IWinoNavigationService.cs
+++ b/Wino.Core.Domain/Interfaces/IWinoNavigationService.cs
@@ -8,9 +8,10 @@ public interface INavigationService
{
bool Navigate(WinoPage page,
object parameter = null,
- NavigationReferenceFrame frame = NavigationReferenceFrame.ShellFrame,
+ NavigationReferenceFrame frame = NavigationReferenceFrame.InnerShellFrame,
NavigationTransitionType transition = NavigationTransitionType.None);
Type GetPageType(WinoPage winoPage);
void GoBack();
+ bool ChangeApplicationMode(WinoApplicationMode mode);
}
diff --git a/Wino.Core/Extensions/GoogleIntegratorExtensions.cs b/Wino.Core/Extensions/GoogleIntegratorExtensions.cs
index bbfe9a90..31c6aa83 100644
--- a/Wino.Core/Extensions/GoogleIntegratorExtensions.cs
+++ b/Wino.Core/Extensions/GoogleIntegratorExtensions.cs
@@ -177,6 +177,15 @@ public static class GoogleIntegratorExtensions
return null;
}
+ ///
+ /// Extracts the timezone string from EventDateTime.
+ /// Returns null for all-day events or if timezone is not specified.
+ ///
+ public static string GetEventTimeZone(EventDateTime eventDateTime)
+ {
+ return eventDateTime?.TimeZone;
+ }
+
///
/// RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545.
///
diff --git a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs
index 18f6e2f7..56368809 100644
--- a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs
+++ b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs
@@ -229,23 +229,37 @@ public static class OutlookIntegratorExtensions
public static DateTimeOffset GetDateTimeOffsetFromDateTimeTimeZone(DateTimeTimeZone dateTimeTimeZone)
{
- if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime) || string.IsNullOrEmpty(dateTimeTimeZone.TimeZone))
+ if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime))
{
- throw new ArgumentException("DateTimeTimeZone is null or empty.");
+ throw new ArgumentException("DateTimeTimeZone or DateTime is null or empty.");
}
try
{
// Parse the DateTime string
- if (DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime))
+ if (!DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime))
+ {
+ throw new ArgumentException("DateTime string is not in a valid format.");
+ }
+
+ // If no timezone is provided, assume UTC
+ if (string.IsNullOrEmpty(dateTimeTimeZone.TimeZone))
+ {
+ return new DateTimeOffset(parsedDateTime, TimeSpan.Zero);
+ }
+
+ try
{
// Get TimeZoneInfo to get the offset
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(dateTimeTimeZone.TimeZone);
TimeSpan offset = timeZoneInfo.GetUtcOffset(parsedDateTime);
return new DateTimeOffset(parsedDateTime, offset);
}
- else
- throw new ArgumentException("DateTime string is not in a valid format.");
+ catch (TimeZoneNotFoundException)
+ {
+ // If timezone is not found, assume UTC as fallback
+ return new DateTimeOffset(parsedDateTime, TimeSpan.Zero);
+ }
}
catch (Exception)
{
diff --git a/Wino.Core/Integration/Processors/GmailChangeProcessor.cs b/Wino.Core/Integration/Processors/GmailChangeProcessor.cs
index e7d6ab90..5fe2f6ca 100644
--- a/Wino.Core/Integration/Processors/GmailChangeProcessor.cs
+++ b/Wino.Core/Integration/Processors/GmailChangeProcessor.cs
@@ -102,6 +102,10 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
DurationInSeconds = totalDurationInSeconds,
Location = string.IsNullOrEmpty(calendarEvent.Location) ? parentRecurringEvent.Location : calendarEvent.Location,
+ // Store timezone information
+ StartTimeZone = GoogleIntegratorExtensions.GetEventTimeZone(calendarEvent.Start) ?? parentRecurringEvent.StartTimeZone,
+ EndTimeZone = GoogleIntegratorExtensions.GetEventTimeZone(calendarEvent.End) ?? parentRecurringEvent.EndTimeZone,
+
// Leave it empty if it's not populated.
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent) == null ? string.Empty : GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
Status = GetStatus(calendarEvent.Status),
@@ -137,6 +141,11 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
EndDateOffset = eventEndDateTimeOffset.Value.Offset,
DurationInSeconds = totalDurationInSeconds,
Location = calendarEvent.Location,
+
+ // Store timezone information from Google Calendar event
+ StartTimeZone = GoogleIntegratorExtensions.GetEventTimeZone(calendarEvent.Start),
+ EndTimeZone = GoogleIntegratorExtensions.GetEventTimeZone(calendarEvent.End),
+
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
Status = GetStatus(calendarEvent.Status),
Title = calendarEvent.Summary,
diff --git a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs
index b597d788..371c6876 100644
--- a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs
+++ b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs
@@ -69,6 +69,11 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
savingItem.EndDateOffset = eventEndDateTimeOffset.Offset;
savingItem.DurationInSeconds = durationInSeconds;
+ // Store the timezone information from the event
+ // This preserves the original timezone from Outlook, allowing proper reconstruction later
+ savingItem.StartTimeZone = calendarEvent.Start?.TimeZone;
+ savingItem.EndTimeZone = calendarEvent.End?.TimeZone;
+
savingItem.Title = calendarEvent.Subject;
savingItem.Description = calendarEvent.Body?.Content;
savingItem.Location = calendarEvent.Location?.DisplayName;
@@ -103,6 +108,34 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
savingItem.OrganizerDisplayName = calendarEvent.Organizer?.EmailAddress?.Name;
savingItem.IsHidden = false;
+ // Set timestamps
+ if (calendarEvent.CreatedDateTime.HasValue)
+ savingItem.CreatedAt = calendarEvent.CreatedDateTime.Value;
+
+ if (calendarEvent.LastModifiedDateTime.HasValue)
+ savingItem.UpdatedAt = calendarEvent.LastModifiedDateTime.Value;
+
+ // Set visibility
+ if (calendarEvent.Sensitivity != null)
+ {
+ savingItem.Visibility = calendarEvent.Sensitivity.Value switch
+ {
+ Sensitivity.Normal => CalendarItemVisibility.Public,
+ Sensitivity.Personal => CalendarItemVisibility.Private,
+ Sensitivity.Private => CalendarItemVisibility.Private,
+ Sensitivity.Confidential => CalendarItemVisibility.Confidential,
+ _ => CalendarItemVisibility.Public
+ };
+ }
+ else
+ {
+ savingItem.Visibility = CalendarItemVisibility.Public;
+ }
+
+ // Set IsLocked based on whether the user is the organizer
+ // Read-only events are those where the current user is not the organizer
+ savingItem.IsLocked = calendarEvent.IsOrganizer.HasValue && !calendarEvent.IsOrganizer.Value;
+
if (calendarEvent.ResponseStatus?.Response != null)
{
switch (calendarEvent.ResponseStatus.Response.Value)
diff --git a/Wino.Core/Services/SynchronizationManager.cs b/Wino.Core/Services/SynchronizationManager.cs
index 563f2074..65c1cf77 100644
--- a/Wino.Core/Services/SynchronizationManager.cs
+++ b/Wino.Core/Services/SynchronizationManager.cs
@@ -299,6 +299,56 @@ public class SynchronizationManager : ISynchronizationManager
return await SynchronizeMailAsync(options, cancellationToken);
}
+ ///
+ /// Handles calendar synchronization for the given account.
+ ///
+ /// Calendar synchronization options
+ /// Cancellation token
+ /// Synchronization result
+ public async Task SynchronizeCalendarAsync(CalendarSynchronizationOptions options,
+ CancellationToken cancellationToken = default)
+ {
+ EnsureInitialized();
+
+ var synchronizer = await GetOrCreateSynchronizerAsync(options.AccountId);
+ if (synchronizer == null)
+ {
+ _logger.Error("Could not find or create synchronizer for account {AccountId}", options.AccountId);
+ return CalendarSynchronizationResult.Failed;
+ }
+
+ _logger.Information("Starting calendar synchronization for account {AccountId} with type {SyncType}",
+ options.AccountId, options.Type);
+
+ try
+ {
+ var result = await synchronizer.SynchronizeCalendarEventsAsync(options, cancellationToken);
+
+ _logger.Information("Calendar synchronization completed for account {AccountId} with state {State}",
+ options.AccountId, result.CompletedState);
+
+ // TODO: Create notifications for new calendar events when INotificationBuilder supports it
+ // if (result.DownloadedEvents?.Any() ?? false)
+ // await _notificationBuilder.CreateCalendarNotificationsAsync(result.DownloadedEvents);
+
+ return result;
+ }
+ catch (AuthenticationAttentionException authEx)
+ {
+ _logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId);
+
+ // Create app notification for authentication attention
+ _notificationBuilder.CreateAttentionRequiredNotification(authEx.Account);
+
+ return CalendarSynchronizationResult.Failed;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Calendar synchronization failed for account {AccountId}", options.AccountId);
+ return CalendarSynchronizationResult.Failed;
+ }
+ }
+
///
/// Downloads a MIME message for the given mail item.
///
diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
index 5c63e609..19ea1421 100644
--- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs
+++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
@@ -1631,8 +1631,8 @@ public class OutlookSynchronizer : WinoSynchronizer
{
@@ -1658,20 +1658,7 @@ public class OutlookSynchronizer : WinoSynchronizer
- {
-
- //requestConfiguration.QueryParameters.StartDateTime = startDate;
- //requestConfiguration.QueryParameters.EndDateTime = endDate;
- });
-
- //var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.ToGetRequestInformation((config) =>
- //{
- // config.QueryParameters.Top = (int)InitialMessageDownloadCountPerFolder;
- // config.QueryParameters.Select = outlookMessageSelectParameters;
- // config.QueryParameters.Orderby = ["receivedDateTime desc"];
- //});
-
+ var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.ToGetRequestInformation();
requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken");
requestInformation.QueryParameters.Add("%24deltatoken", currentDeltaToken);
@@ -1726,11 +1713,12 @@ public class OutlookSynchronizer : WinoSynchronizer,
IRecipient,
IRecipient,
@@ -83,7 +83,7 @@ public partial class AppShellViewModel : MailBaseViewModel,
private readonly SemaphoreSlim accountInitFolderUpdateSlim = new SemaphoreSlim(1);
- public AppShellViewModel(IMailDialogService dialogService,
+ public MailAppShellViewModel(IMailDialogService dialogService,
INavigationService navigationService,
IMimeFileService mimeFileService,
INativeAppService nativeAppService,
@@ -392,7 +392,7 @@ public partial class AppShellViewModel : MailBaseViewModel,
var args = new NavigateMailFolderEventArgs(baseFolderMenuItem, folderInitAwaitTask);
- NavigationService.Navigate(WinoPage.MailListPage, args, NavigationReferenceFrame.ShellFrame);
+ NavigationService.Navigate(WinoPage.MailListPage, args, NavigationReferenceFrame.InnerShellFrame);
UpdateWindowTitleForFolder(baseFolderMenuItem);
});
@@ -587,15 +587,15 @@ public partial class AppShellViewModel : MailBaseViewModel,
}
else if (clickedMenuItem is SettingsItem)
{
- NavigationService.Navigate(WinoPage.SettingsPage, parameter, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None);
+ NavigationService.Navigate(WinoPage.SettingsPage, parameter, NavigationReferenceFrame.InnerShellFrame, NavigationTransitionType.None);
}
else if (clickedMenuItem is ManageAccountsMenuItem)
{
- NavigationService.Navigate(WinoPage.ManageAccountsPage, parameter, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None);
+ NavigationService.Navigate(WinoPage.ManageAccountsPage, parameter, NavigationReferenceFrame.InnerShellFrame, NavigationTransitionType.None);
}
else if (clickedMenuItem is ContactsMenuItem)
{
- NavigationService.Navigate(WinoPage.ContactsPage, parameter, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None);
+ NavigationService.Navigate(WinoPage.ContactsPage, parameter, NavigationReferenceFrame.InnerShellFrame, NavigationTransitionType.None);
}
else if (clickedMenuItem is IAccountMenuItem clickedAccountMenuItem)
{
@@ -1003,7 +1003,7 @@ public partial class AppShellViewModel : MailBaseViewModel,
accountMenuItem.TotalItemsToSync = message.TotalItemsToSync;
accountMenuItem.RemainingItemsToSync = message.RemainingItemsToSync;
accountMenuItem.SynchronizationStatus = message.SynchronizationStatus;
-
+
// If this account is part of a merged inbox, update the merged inbox progress as well
if (accountMenuItem.ParentMenuItem is MergedAccountMenuItem mergedAccountMenuItem)
{
@@ -1040,6 +1040,9 @@ public partial class AppShellViewModel : MailBaseViewModel,
{
base.UnregisterRecipients();
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
+ Messenger.Unregister(this);
Messenger.Unregister(this);
Messenger.Unregister(this);
Messenger.Unregister(this);
diff --git a/Wino.Mail.ViewModels/MailBaseViewModel.cs b/Wino.Mail.ViewModels/MailBaseViewModel.cs
index 75887487..e27324ce 100644
--- a/Wino.Mail.ViewModels/MailBaseViewModel.cs
+++ b/Wino.Mail.ViewModels/MailBaseViewModel.cs
@@ -43,7 +43,9 @@ public class MailBaseViewModel : CoreBaseViewModel,
protected override void RegisterRecipients()
{
base.RegisterRecipients();
-
+
+ UnregisterRecipients();
+
Messenger.Register(this);
Messenger.Register(this);
Messenger.Register(this);
@@ -58,7 +60,7 @@ public class MailBaseViewModel : CoreBaseViewModel,
protected override void UnregisterRecipients()
{
base.UnregisterRecipients();
-
+
Messenger.Unregister(this);
Messenger.Unregister(this);
Messenger.Unregister(this);
diff --git a/Wino.Mail.ViewModels/MailViewModelsContainerSetup.cs b/Wino.Mail.ViewModels/MailViewModelsContainerSetup.cs
deleted file mode 100644
index d1526993..00000000
--- a/Wino.Mail.ViewModels/MailViewModelsContainerSetup.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Wino.Core;
-
-namespace Wino.Mail.ViewModels;
-
-public static class MailViewModelsContainerSetup
-{
- public static void RegisterViewModelService(this IServiceCollection services)
- {
- // View models use core services.
- services.RegisterCoreServices();
- }
-}
diff --git a/Wino.Mail.WinUI/App.xaml b/Wino.Mail.WinUI/App.xaml
index de6023e7..c9a30e23 100644
--- a/Wino.Mail.WinUI/App.xaml
+++ b/Wino.Mail.WinUI/App.xaml
@@ -18,7 +18,14 @@
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs
index 052c5a75..10e8722b 100644
--- a/Wino.Mail.WinUI/App.xaml.cs
+++ b/Wino.Mail.WinUI/App.xaml.cs
@@ -9,6 +9,9 @@ using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Microsoft.Windows.AppNotifications;
using MimeKit.Cryptography;
+using Wino.Calendar.ViewModels;
+using Wino.Calendar.ViewModels.Interfaces;
+using Wino.Core;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
@@ -17,12 +20,15 @@ using Wino.Core.Domain.Models.Synchronization;
using Wino.Mail.Services;
using Wino.Mail.ViewModels;
using Wino.Mail.WinUI.Interfaces;
+using Wino.Mail.WinUI.Services;
using Wino.Messaging.Client.Accounts;
using Wino.Messaging.Server;
using Wino.Services;
namespace Wino.Mail.WinUI;
-public partial class App : WinoApplication, IRecipient
+public partial class App : WinoApplication,
+ IRecipient,
+ IRecipient
{
private ISynchronizationManager? _synchronizationManager;
@@ -60,11 +66,13 @@ public partial class App : WinoApplication, IRecipient();
services.AddTransient();
services.AddSingleton();
+ services.AddSingleton();
}
private void RegisterViewModels(IServiceCollection services)
{
- services.AddSingleton(typeof(AppShellViewModel));
+ services.AddSingleton(typeof(MailAppShellViewModel));
+ services.AddSingleton(typeof(CalendarAppShellViewModel));
services.AddTransient(typeof(MailListPageViewModel));
services.AddTransient(typeof(MailRenderingPageViewModel));
@@ -85,6 +93,10 @@ public partial class App : WinoApplication, IRecipient(this);
+ WeakReferenceMessenger.Default.Register(this);
}
- public void Receive(NewMailSynchronizationRequested message)
- {
- _synchronizationManager?.SynchronizeMailAsync(message.Options);
- }
+ public void Receive(NewMailSynchronizationRequested message) => _synchronizationManager?.SynchronizeMailAsync(message.Options);
+ public void Receive(NewCalendarSynchronizationRequested message) => _synchronizationManager?.SynchronizeCalendarAsync(message.Options);
///
/// Handles activation redirected from another instance (single-instancing).
@@ -378,4 +389,5 @@ public partial class App : WinoApplication, IRecipient
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs
new file mode 100644
index 00000000..e714cd5e
--- /dev/null
+++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs
@@ -0,0 +1,197 @@
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.Messaging;
+using Itenso.TimePeriod;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
+using Wino.Calendar.ViewModels.Data;
+using Wino.Calendar.ViewModels.Messages;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Models.Calendar;
+
+namespace Wino.Calendar.Controls;
+
+public sealed partial class CalendarItemControl : UserControl
+{
+ // Single tap has a delay to report double taps properly.
+ private bool isSingleTap = false;
+
+ public static readonly DependencyProperty CalendarItemProperty = DependencyProperty.Register(nameof(CalendarItem), typeof(CalendarItemViewModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarItemChanged)));
+ public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.Register(nameof(IsDragging), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false));
+ public static readonly DependencyProperty IsCustomEventAreaProperty = DependencyProperty.Register(nameof(IsCustomEventArea), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false));
+ public static readonly DependencyProperty CalendarItemTitleProperty = DependencyProperty.Register(nameof(CalendarItemTitle), typeof(string), typeof(CalendarItemControl), new PropertyMetadata(string.Empty));
+ public static readonly DependencyProperty DisplayingDateProperty = DependencyProperty.Register(nameof(DisplayingDate), typeof(CalendarDayModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnDisplayDateChanged)));
+
+ ///
+ /// Whether the control is displaying as regular event or all-multi day area in the day control.
+ ///
+ public bool IsCustomEventArea
+ {
+ get { return (bool)GetValue(IsCustomEventAreaProperty); }
+ set { SetValue(IsCustomEventAreaProperty, value); }
+ }
+
+ ///
+ /// Day that the calendar item is rendered at.
+ /// It's needed for title manipulation and some other adjustments later on.
+ ///
+ public CalendarDayModel DisplayingDate
+ {
+ get { return (CalendarDayModel)GetValue(DisplayingDateProperty); }
+ set { SetValue(DisplayingDateProperty, value); }
+ }
+
+ public string CalendarItemTitle
+ {
+ get { return (string)GetValue(CalendarItemTitleProperty); }
+ set { SetValue(CalendarItemTitleProperty, value); }
+ }
+
+ public CalendarItemViewModel CalendarItem
+ {
+ get { return (CalendarItemViewModel)GetValue(CalendarItemProperty); }
+ set { SetValue(CalendarItemProperty, value); }
+ }
+
+ public bool IsDragging
+ {
+ get { return (bool)GetValue(IsDraggingProperty); }
+ set { SetValue(IsDraggingProperty, value); }
+ }
+
+ public CalendarItemControl()
+ {
+ InitializeComponent();
+ }
+
+ private static void OnDisplayDateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is CalendarItemControl control)
+ {
+ control.UpdateControlVisuals();
+ }
+ }
+
+ private static void OnCalendarItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is CalendarItemControl control)
+ {
+ control.UpdateControlVisuals();
+ }
+ }
+
+ private void UpdateControlVisuals()
+ {
+ // Depending on the calendar item's duration and attributes, we might need to change the display title.
+ // 1. Multi-Day events should display the start date and end date.
+ // 2. Multi-Day events that occupy the whole day just shows 'all day'.
+ // 3. Other events should display the title.
+
+ if (CalendarItem == null) return;
+ if (DisplayingDate == null) return;
+
+ if (CalendarItem.IsMultiDayEvent)
+ {
+ // Multi day events are divided into 3 categories:
+ // 1. All day events
+ // 2. Events that started after the period.
+ // 3. Events that started before the period and finishes within the period.
+
+ var periodRelation = CalendarItem.Period.GetRelation(DisplayingDate.Period);
+
+ if (periodRelation == Itenso.TimePeriod.PeriodRelation.StartInside ||
+ periodRelation == PeriodRelation.EnclosingStartTouching)
+ {
+ // hour -> title
+ CalendarItemTitle = $"{DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.StartDate.TimeOfDay)} -> {CalendarItem.Title}";
+ }
+ else if (
+ periodRelation == PeriodRelation.EndInside ||
+ periodRelation == PeriodRelation.EnclosingEndTouching)
+ {
+ // title <- hour
+ CalendarItemTitle = $"{CalendarItem.Title} <- {DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.EndDate.TimeOfDay)}";
+ }
+ else if (periodRelation == PeriodRelation.Enclosing)
+ {
+ // This event goes all day and it's multi-day.
+ // Item must be hidden in the calendar but displayed on the custom area at the top.
+
+ CalendarItemTitle = $"{Translator.CalendarItemAllDay} {CalendarItem.Title}";
+ }
+ else
+ {
+ // Not expected, but there it is.
+ CalendarItemTitle = CalendarItem.Title;
+ }
+
+ // Debug.WriteLine($"{CalendarItem.Title} Period relation with {DisplayingDate.Period.ToString()}: {periodRelation}");
+ }
+ else
+ {
+ CalendarItemTitle = CalendarItem.Title;
+ }
+
+ UpdateVisualStates();
+ }
+
+ private void UpdateVisualStates()
+ {
+ if (CalendarItem == null) return;
+
+ if (CalendarItem.IsAllDayEvent)
+ {
+ VisualStateManager.GoToState(this, "AllDayEvent", true);
+ }
+ else if (CalendarItem.IsMultiDayEvent)
+ {
+ if (IsCustomEventArea)
+ {
+ VisualStateManager.GoToState(this, "CustomAreaMultiDayEvent", true);
+ }
+ else
+ {
+ // Hide it.
+ VisualStateManager.GoToState(this, "MultiDayEvent", true);
+ }
+ }
+ else
+ {
+ VisualStateManager.GoToState(this, "RegularEvent", true);
+ }
+ }
+
+ private void ControlDragStarting(UIElement sender, DragStartingEventArgs args) => IsDragging = true;
+
+ private void ControlDropped(UIElement sender, DropCompletedEventArgs args) => IsDragging = false;
+
+ private async void ControlTapped(object sender, TappedRoutedEventArgs e)
+ {
+ if (CalendarItem == null) return;
+
+ isSingleTap = true;
+
+ await Task.Delay(100);
+
+ if (isSingleTap)
+ {
+ WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem, DisplayingDate));
+ }
+ }
+
+ private void ControlDoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
+ {
+ if (CalendarItem == null) return;
+
+ isSingleTap = false;
+
+ WeakReferenceMessenger.Default.Send(new CalendarItemDoubleTappedMessage(CalendarItem));
+ }
+
+ private void ControlRightTapped(object sender, RightTappedRoutedEventArgs e)
+ {
+ if (CalendarItem == null) return;
+
+ WeakReferenceMessenger.Default.Send(new CalendarItemRightTappedMessage(CalendarItem));
+ }
+}
diff --git a/Wino.Mail.WinUI/Controls/Calendar/CustomCalendarFlipView.cs b/Wino.Mail.WinUI/Controls/Calendar/CustomCalendarFlipView.cs
new file mode 100644
index 00000000..3cc32ec7
--- /dev/null
+++ b/Wino.Mail.WinUI/Controls/Calendar/CustomCalendarFlipView.cs
@@ -0,0 +1,42 @@
+using Microsoft.UI.Xaml.Automation.Peers;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Wino.Calendar.Controls;
+
+///
+/// FlipView that hides the navigation buttons and exposes methods to navigate to the next and previous items with animations.
+///
+public partial class CustomCalendarFlipView : FlipView
+{
+ private const string PART_PreviousButton = "PreviousButtonHorizontal";
+ private const string PART_NextButton = "NextButtonHorizontal";
+
+ private Button PreviousButton;
+ private Button NextButton;
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ PreviousButton = GetTemplateChild(PART_PreviousButton) as Button;
+ NextButton = GetTemplateChild(PART_NextButton) as Button;
+
+ // Hide navigation buttons
+ PreviousButton.Opacity = NextButton.Opacity = 0;
+ PreviousButton.IsHitTestVisible = NextButton.IsHitTestVisible = false;
+
+ var t = FindName("ScrollingHost");
+ }
+
+ public void GoPreviousFlip()
+ {
+ var backPeer = new ButtonAutomationPeer(PreviousButton);
+ backPeer.Invoke();
+ }
+
+ public void GoNextFlip()
+ {
+ var nextPeer = new ButtonAutomationPeer(NextButton);
+ nextPeer.Invoke();
+ }
+}
diff --git a/Wino.Mail.WinUI/Controls/Calendar/DayColumnControl.cs b/Wino.Mail.WinUI/Controls/Calendar/DayColumnControl.cs
new file mode 100644
index 00000000..50ccc128
--- /dev/null
+++ b/Wino.Mail.WinUI/Controls/Calendar/DayColumnControl.cs
@@ -0,0 +1,77 @@
+using System;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Wino.Core.Domain.Models.Calendar;
+
+namespace Wino.Calendar.Controls;
+
+public partial class DayColumnControl : Control
+{
+ private const string PART_HeaderDateDayText = nameof(PART_HeaderDateDayText);
+ private const string PART_IsTodayBorder = nameof(PART_IsTodayBorder);
+ private const string PART_ColumnHeaderText = nameof(PART_ColumnHeaderText);
+
+ private const string PART_AllDayItemsControl = nameof(PART_AllDayItemsControl);
+
+ private const string TodayState = nameof(TodayState);
+ private const string NotTodayState = nameof(NotTodayState);
+
+ private TextBlock HeaderDateDayText;
+ private TextBlock ColumnHeaderText;
+ private Border IsTodayBorder;
+ private ItemsControl AllDayItemsControl;
+
+ public CalendarDayModel DayModel
+ {
+ get { return (CalendarDayModel)GetValue(DayModelProperty); }
+ set { SetValue(DayModelProperty, value); }
+ }
+
+ public static readonly DependencyProperty DayModelProperty = DependencyProperty.Register(nameof(DayModel), typeof(CalendarDayModel), typeof(DayColumnControl), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
+
+ public DayColumnControl()
+ {
+ DefaultStyleKey = typeof(DayColumnControl);
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ HeaderDateDayText = GetTemplateChild(PART_HeaderDateDayText) as TextBlock;
+ ColumnHeaderText = GetTemplateChild(PART_ColumnHeaderText) as TextBlock;
+ IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border;
+ AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as ItemsControl;
+
+ UpdateValues();
+ }
+
+ private static void OnRenderingPropertiesChanged(DependencyObject control, DependencyPropertyChangedEventArgs e)
+ {
+ if (control is DayColumnControl columnControl)
+ {
+ columnControl.UpdateValues();
+ }
+ }
+
+ private void UpdateValues()
+ {
+ if (HeaderDateDayText == null || IsTodayBorder == null || DayModel == null) return;
+
+ HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString();
+
+ // Monthly template does not use it.
+ if (ColumnHeaderText != null)
+ {
+ ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo);
+ }
+
+ AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents;
+
+ bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date;
+
+ VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false);
+
+ UpdateLayout();
+ }
+}
diff --git a/Wino.Mail.WinUI/Controls/Calendar/DayHeaderControl.cs b/Wino.Mail.WinUI/Controls/Calendar/DayHeaderControl.cs
new file mode 100644
index 00000000..d7154ef2
--- /dev/null
+++ b/Wino.Mail.WinUI/Controls/Calendar/DayHeaderControl.cs
@@ -0,0 +1,56 @@
+using System;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Wino.Core.Domain.Enums;
+
+namespace Wino.Calendar.Controls;
+
+public partial class DayHeaderControl : Control
+{
+ private const string PART_DayHeaderTextBlock = nameof(PART_DayHeaderTextBlock);
+ private TextBlock HeaderTextblock;
+
+ public DayHeaderDisplayType DisplayType
+ {
+ get { return (DayHeaderDisplayType)GetValue(DisplayTypeProperty); }
+ set { SetValue(DisplayTypeProperty, value); }
+ }
+
+ public DateTime Date
+ {
+ get { return (DateTime)GetValue(DateProperty); }
+ set { SetValue(DateProperty, value); }
+ }
+
+ public static readonly DependencyProperty DateProperty = DependencyProperty.Register(nameof(Date), typeof(DateTime), typeof(DayHeaderControl), new PropertyMetadata(default(DateTime), new PropertyChangedCallback(OnHeaderPropertyChanged)));
+ public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(DayHeaderDisplayType), typeof(DayHeaderControl), new PropertyMetadata(DayHeaderDisplayType.TwentyFourHour, new PropertyChangedCallback(OnHeaderPropertyChanged)));
+
+ public DayHeaderControl()
+ {
+ DefaultStyleKey = typeof(DayHeaderControl);
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ HeaderTextblock = GetTemplateChild(PART_DayHeaderTextBlock) as TextBlock;
+ UpdateHeaderText();
+ }
+
+ private static void OnHeaderPropertyChanged(DependencyObject control, DependencyPropertyChangedEventArgs e)
+ {
+ if (control is DayHeaderControl headerControl)
+ {
+ headerControl.UpdateHeaderText();
+ }
+ }
+
+ private void UpdateHeaderText()
+ {
+ if (HeaderTextblock != null)
+ {
+ HeaderTextblock.Text = DisplayType == DayHeaderDisplayType.TwelveHour ? Date.ToString("h tt") : Date.ToString("HH:mm");
+ }
+ }
+}
diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarControl.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarControl.cs
new file mode 100644
index 00000000..eb32d8bb
--- /dev/null
+++ b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarControl.cs
@@ -0,0 +1,299 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Wino.Calendar.Args;
+using Wino.Calendar.ViewModels.Data;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Calendar;
+using Wino.Helpers;
+
+namespace Wino.Calendar.Controls;
+
+public partial class WinoCalendarControl : Control
+{
+ private const string PART_WinoFlipView = nameof(PART_WinoFlipView);
+ private const string PART_IdleGrid = nameof(PART_IdleGrid);
+
+ public event EventHandler TimelineCellSelected;
+ public event EventHandler TimelineCellUnselected;
+
+ public event EventHandler ScrollPositionChanging;
+
+ #region Dependency Properties
+
+ public static readonly DependencyProperty DayRangesProperty = DependencyProperty.Register(nameof(DayRanges), typeof(ObservableCollection), typeof(WinoCalendarControl), new PropertyMetadata(null));
+ public static readonly DependencyProperty SelectedFlipViewIndexProperty = DependencyProperty.Register(nameof(SelectedFlipViewIndex), typeof(int), typeof(WinoCalendarControl), new PropertyMetadata(-1));
+ public static readonly DependencyProperty SelectedFlipViewDayRangeProperty = DependencyProperty.Register(nameof(SelectedFlipViewDayRange), typeof(DayRangeRenderModel), typeof(WinoCalendarControl), new PropertyMetadata(null));
+ public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnActiveCanvasChanged)));
+ public static readonly DependencyProperty IsFlipIdleProperty = DependencyProperty.Register(nameof(IsFlipIdle), typeof(bool), typeof(WinoCalendarControl), new PropertyMetadata(true, new PropertyChangedCallback(OnIdleStateChanged)));
+ public static readonly DependencyProperty ActiveScrollViewerProperty = DependencyProperty.Register(nameof(ActiveScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnActiveVerticalScrollViewerChanged)));
+
+ public static readonly DependencyProperty VerticalItemsPanelTemplateProperty = DependencyProperty.Register(nameof(VerticalItemsPanelTemplate), typeof(ItemsPanelTemplate), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated)));
+ public static readonly DependencyProperty HorizontalItemsPanelTemplateProperty = DependencyProperty.Register(nameof(HorizontalItemsPanelTemplate), typeof(ItemsPanelTemplate), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated)));
+ public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(CalendarOrientation), typeof(WinoCalendarControl), new PropertyMetadata(CalendarOrientation.Horizontal, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated)));
+ public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(CalendarDisplayType), typeof(WinoCalendarControl), new PropertyMetadata(CalendarDisplayType.Day));
+
+ ///
+ /// Gets or sets the day-week-month-year display type.
+ /// Orientation is not determined by this property, but Orientation property.
+ /// This property is used to determine the template to use for the calendar.
+ ///
+ public CalendarDisplayType DisplayType
+ {
+ get { return (CalendarDisplayType)GetValue(DisplayTypeProperty); }
+ set { SetValue(DisplayTypeProperty, value); }
+ }
+
+ public CalendarOrientation Orientation
+ {
+ get { return (CalendarOrientation)GetValue(OrientationProperty); }
+ set { SetValue(OrientationProperty, value); }
+ }
+
+ public ItemsPanelTemplate VerticalItemsPanelTemplate
+ {
+ get { return (ItemsPanelTemplate)GetValue(VerticalItemsPanelTemplateProperty); }
+ set { SetValue(VerticalItemsPanelTemplateProperty, value); }
+ }
+
+ public ItemsPanelTemplate HorizontalItemsPanelTemplate
+ {
+ get { return (ItemsPanelTemplate)GetValue(HorizontalItemsPanelTemplateProperty); }
+ set { SetValue(HorizontalItemsPanelTemplateProperty, value); }
+ }
+
+ public DayRangeRenderModel SelectedFlipViewDayRange
+ {
+ get { return (DayRangeRenderModel)GetValue(SelectedFlipViewDayRangeProperty); }
+ set { SetValue(SelectedFlipViewDayRangeProperty, value); }
+ }
+
+ public ScrollViewer ActiveScrollViewer
+ {
+ get { return (ScrollViewer)GetValue(ActiveScrollViewerProperty); }
+ set { SetValue(ActiveScrollViewerProperty, value); }
+ }
+
+ public WinoDayTimelineCanvas ActiveCanvas
+ {
+ get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); }
+ set { SetValue(ActiveCanvasProperty, value); }
+ }
+
+ public bool IsFlipIdle
+ {
+ get { return (bool)GetValue(IsFlipIdleProperty); }
+ set { SetValue(IsFlipIdleProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the collection of day ranges to render.
+ /// Each day range usually represents a week, but it may support other ranges.
+ ///
+ public ObservableCollection DayRanges
+ {
+ get { return (ObservableCollection)GetValue(DayRangesProperty); }
+ set { SetValue(DayRangesProperty, value); }
+ }
+
+ public int SelectedFlipViewIndex
+ {
+ get { return (int)GetValue(SelectedFlipViewIndexProperty); }
+ set { SetValue(SelectedFlipViewIndexProperty, value); }
+ }
+
+ #endregion
+
+ private WinoCalendarFlipView InternalFlipView;
+ private Grid IdleGrid;
+
+ public WinoCalendarControl()
+ {
+ DefaultStyleKey = typeof(WinoCalendarControl);
+ SizeChanged += CalendarSizeChanged;
+ }
+
+ private static void OnCalendarOrientationPropertiesUpdated(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
+ {
+ if (calendar is WinoCalendarControl control)
+ {
+ control.ManageCalendarOrientation();
+ }
+ }
+
+ private static void OnIdleStateChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
+ {
+ if (calendar is WinoCalendarControl calendarControl)
+ {
+ calendarControl.UpdateIdleState();
+ }
+ }
+
+
+ private static void OnActiveVerticalScrollViewerChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
+ {
+ if (calendar is WinoCalendarControl calendarControl)
+ {
+ if (e.OldValue is ScrollViewer oldScrollViewer)
+ {
+ calendarControl.DeregisterScrollChanges(oldScrollViewer);
+ }
+
+ if (e.NewValue is ScrollViewer newScrollViewer)
+ {
+ calendarControl.RegisterScrollChanges(newScrollViewer);
+ }
+
+ calendarControl.ManageHighlightedDateRange();
+ }
+ }
+
+
+ private static void OnActiveCanvasChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
+ {
+ if (calendar is WinoCalendarControl calendarControl)
+ {
+ if (e.OldValue is WinoDayTimelineCanvas oldCanvas)
+ {
+ // Dismiss any selection on the old canvas.
+ calendarControl.DeregisterCanvas(oldCanvas);
+ }
+
+ if (e.NewValue is WinoDayTimelineCanvas newCanvas)
+ {
+ calendarControl.RegisterCanvas(newCanvas);
+ }
+
+ calendarControl.ManageHighlightedDateRange();
+ }
+ }
+
+ private void ManageCalendarOrientation()
+ {
+ if (InternalFlipView == null || HorizontalItemsPanelTemplate == null || VerticalItemsPanelTemplate == null) return;
+
+ InternalFlipView.ItemsPanel = Orientation == CalendarOrientation.Horizontal ? HorizontalItemsPanelTemplate : VerticalItemsPanelTemplate;
+ }
+
+ private void ManageHighlightedDateRange()
+ => SelectedFlipViewDayRange = InternalFlipView.SelectedItem as DayRangeRenderModel;
+
+ private void DeregisterCanvas(WinoDayTimelineCanvas canvas)
+ {
+ if (canvas == null) return;
+
+ canvas.SelectedDateTime = null;
+ canvas.TimelineCellSelected -= ActiveTimelineCellSelected;
+ canvas.TimelineCellUnselected -= ActiveTimelineCellUnselected;
+ }
+
+ private void RegisterCanvas(WinoDayTimelineCanvas canvas)
+ {
+ if (canvas == null) return;
+
+ canvas.SelectedDateTime = null;
+ canvas.TimelineCellSelected += ActiveTimelineCellSelected;
+ canvas.TimelineCellUnselected += ActiveTimelineCellUnselected;
+ }
+
+ private void RegisterScrollChanges(ScrollViewer scrollViewer)
+ {
+ if (scrollViewer == null) return;
+
+ scrollViewer.ViewChanging += ScrollViewChanging;
+ }
+
+ private void DeregisterScrollChanges(ScrollViewer scrollViewer)
+ {
+ if (scrollViewer == null) return;
+
+ scrollViewer.ViewChanging -= ScrollViewChanging;
+ }
+
+ private void ScrollViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
+ => ScrollPositionChanging?.Invoke(this, EventArgs.Empty);
+
+ private void CalendarSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if (ActiveCanvas == null) return;
+
+ ActiveCanvas.SelectedDateTime = null;
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ InternalFlipView = GetTemplateChild(PART_WinoFlipView) as WinoCalendarFlipView;
+ IdleGrid = GetTemplateChild(PART_IdleGrid) as Grid;
+
+ UpdateIdleState();
+ ManageCalendarOrientation();
+ }
+
+ private void UpdateIdleState()
+ {
+ InternalFlipView.Opacity = IsFlipIdle ? 0 : 1;
+ IdleGrid.Visibility = IsFlipIdle ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ private void ActiveTimelineCellUnselected(object sender, TimelineCellUnselectedArgs e)
+ => TimelineCellUnselected?.Invoke(this, e);
+
+ private void ActiveTimelineCellSelected(object sender, TimelineCellSelectedArgs e)
+ => TimelineCellSelected?.Invoke(this, e);
+
+ public void NavigateToDay(DateTime dateTime) => InternalFlipView.NavigateToDay(dateTime);
+
+ public async void NavigateToHour(TimeSpan timeSpan)
+ {
+ if (ActiveScrollViewer == null) return;
+
+ // Total height of the FlipViewItem is the same as vertical ScrollViewer to position day headers.
+
+ await Task.Yield();
+ await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () =>
+ {
+ double hourHeght = 60;
+ double totalHeight = ActiveScrollViewer.ScrollableHeight;
+ double scrollPosition = timeSpan.TotalHours * hourHeght;
+
+ ActiveScrollViewer.ChangeView(null, scrollPosition, null, disableAnimation: false);
+ });
+ }
+ public void ResetTimelineSelection()
+ {
+ if (ActiveCanvas == null) return;
+
+ ActiveCanvas.SelectedDateTime = null;
+ }
+
+ public void GoNextRange()
+ {
+ if (InternalFlipView == null) return;
+
+ InternalFlipView.GoNextFlip();
+ }
+
+ public void GoPreviousRange()
+ {
+ if (InternalFlipView == null) return;
+
+ InternalFlipView.GoPreviousFlip();
+ }
+
+ public void UnselectActiveTimelineCell()
+ {
+ if (ActiveCanvas == null) return;
+
+ ActiveCanvas.SelectedDateTime = null;
+ }
+
+ public CalendarItemControl GetCalendarItemControl(CalendarItemViewModel calendarItemViewModel)
+ {
+ return this.FindDescendants().FirstOrDefault(a => a.CalendarItem == calendarItemViewModel);
+ }
+}
diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarFlipView.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarFlipView.cs
new file mode 100644
index 00000000..0270b002
--- /dev/null
+++ b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarFlipView.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.WinUI;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Wino.Core.Domain.Collections;
+using Wino.Core.Domain.Models.Calendar;
+
+namespace Wino.Calendar.Controls;
+
+public partial class WinoCalendarFlipView : CustomCalendarFlipView
+{
+ public static readonly DependencyProperty IsIdleProperty = DependencyProperty.Register(nameof(IsIdle), typeof(bool), typeof(WinoCalendarFlipView), new PropertyMetadata(true));
+ public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarFlipView), new PropertyMetadata(null));
+ public static readonly DependencyProperty ActiveVerticalScrollViewerProperty = DependencyProperty.Register(nameof(ActiveVerticalScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarFlipView), new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets the active canvas that is currently displayed in the flip view.
+ /// Each day-range of flip view item has a canvas that displays the day timeline.
+ ///
+ public WinoDayTimelineCanvas ActiveCanvas
+ {
+ get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); }
+ set { SetValue(ActiveCanvasProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the scroll viewer that is currently active in the flip view.
+ /// It's the vertical scroll that scrolls the timeline only, not the header part that belongs
+ /// to parent FlipView control.
+ ///
+ public ScrollViewer ActiveVerticalScrollViewer
+ {
+ get { return (ScrollViewer)GetValue(ActiveVerticalScrollViewerProperty); }
+ set { SetValue(ActiveVerticalScrollViewerProperty, value); }
+ }
+
+ public bool IsIdle
+ {
+ get { return (bool)GetValue(IsIdleProperty); }
+ set { SetValue(IsIdleProperty, value); }
+ }
+
+ public WinoCalendarFlipView()
+ {
+ RegisterPropertyChangedCallback(SelectedIndexProperty, new DependencyPropertyChangedCallback(OnSelectedIndexUpdated));
+ RegisterPropertyChangedCallback(ItemsSourceProperty, new DependencyPropertyChangedCallback(OnItemsSourceChanged));
+ }
+
+ private static void OnItemsSourceChanged(DependencyObject d, DependencyProperty e)
+ {
+ if (d is WinoCalendarFlipView flipView)
+ {
+ flipView.RegisterItemsSourceChange();
+ }
+ }
+
+ private static void OnSelectedIndexUpdated(DependencyObject d, DependencyProperty e)
+ {
+ if (d is WinoCalendarFlipView flipView)
+ {
+ flipView.UpdateActiveCanvas();
+ flipView.UpdateActiveScrollViewer();
+ }
+ }
+
+ private void RegisterItemsSourceChange()
+ {
+ if (GetItemsSource() is INotifyCollectionChanged notifyCollectionChanged)
+ {
+ notifyCollectionChanged.CollectionChanged += ItemsSourceUpdated;
+ }
+ }
+
+ private void ItemsSourceUpdated(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ IsIdle = e.Action == NotifyCollectionChangedAction.Reset || e.Action == NotifyCollectionChangedAction.Replace;
+ }
+
+ private async Task GetCurrentFlipViewItem()
+ {
+ // TODO: Refactor this mechanism by listening to PrepareContainerForItemOverride and Loaded events together.
+ while (ContainerFromIndex(SelectedIndex) == null)
+ {
+ await Task.Delay(100);
+ }
+
+ return ContainerFromIndex(SelectedIndex) as FlipViewItem;
+
+
+ }
+
+ private void UpdateActiveScrollViewer()
+ {
+ if (SelectedIndex < 0)
+ ActiveVerticalScrollViewer = null;
+ else
+ {
+ GetCurrentFlipViewItem().ContinueWith(task =>
+ {
+ if (task.IsCompletedSuccessfully)
+ {
+ var flipViewItem = task.Result;
+
+ _ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
+ {
+ ActiveVerticalScrollViewer = flipViewItem.FindDescendant();
+ });
+ }
+ });
+ }
+ }
+
+ public void UpdateActiveCanvas()
+ {
+ if (SelectedIndex < 0)
+ ActiveCanvas = null;
+ else
+ {
+ GetCurrentFlipViewItem().ContinueWith(task =>
+ {
+ if (task.IsCompletedSuccessfully)
+ {
+ var flipViewItem = task.Result;
+
+ _ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
+ {
+ ActiveCanvas = flipViewItem.FindDescendant();
+ });
+ }
+ });
+ }
+ }
+
+ ///
+ /// Navigates to the specified date in the calendar.
+ ///
+ /// Date to navigate.
+ public async void NavigateToDay(DateTime dateTime)
+ {
+ await Task.Yield();
+
+ await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () =>
+ {
+ // Find the day range that contains the date.
+ var dayRange = GetItemsSource()?.FirstOrDefault(a => a.CalendarDays.Any(b => b.RepresentingDate.Date == dateTime.Date));
+
+ if (dayRange != null)
+ {
+ var navigationItemIndex = GetItemsSource().IndexOf(dayRange);
+
+ if (Math.Abs(navigationItemIndex - SelectedIndex) > 4)
+ {
+ // Difference between dates are high.
+ // No need to animate this much, just go without animating.
+
+ SelectedIndex = navigationItemIndex;
+ }
+ else
+ {
+ // Until we reach the day in the flip, simulate next-prev button clicks.
+ // This will make sure the FlipView animations are triggered.
+ // Setting SelectedIndex directly doesn't trigger the animations.
+
+ while (SelectedIndex != navigationItemIndex)
+ {
+ if (SelectedIndex > navigationItemIndex)
+ {
+ GoPreviousFlip();
+ }
+ else
+ {
+ GoNextFlip();
+ }
+ }
+ }
+ }
+ });
+ }
+
+ private ObservableRangeCollection GetItemsSource()
+ => ItemsSource as ObservableRangeCollection;
+}
diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarPanel.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarPanel.cs
new file mode 100644
index 00000000..76e29ed7
--- /dev/null
+++ b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarPanel.cs
@@ -0,0 +1,293 @@
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using CommunityToolkit.WinUI;
+using Itenso.TimePeriod;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Windows.Foundation;
+using Wino.Calendar.Models;
+using Wino.Calendar.ViewModels.Data;
+using Wino.Core.Domain.Interfaces;
+
+namespace Wino.Calendar.Controls;
+
+public partial class WinoCalendarPanel : Panel
+{
+ private const double LastItemRightExtraMargin = 12d;
+
+ // Store each ICalendarItem measurements by their Id.
+ private readonly Dictionary _measurements = new Dictionary();
+
+ public static readonly DependencyProperty EventItemMarginProperty = DependencyProperty.Register(nameof(EventItemMargin), typeof(Thickness), typeof(WinoCalendarPanel), new PropertyMetadata(new Thickness(0, 0, 0, 0)));
+ public static readonly DependencyProperty HourHeightProperty = DependencyProperty.Register(nameof(HourHeight), typeof(double), typeof(WinoCalendarPanel), new PropertyMetadata(0d));
+ public static readonly DependencyProperty PeriodProperty = DependencyProperty.Register(nameof(Period), typeof(ITimePeriod), typeof(WinoCalendarPanel), new PropertyMetadata(null));
+
+ public ITimePeriod Period
+ {
+ get { return (ITimePeriod)GetValue(PeriodProperty); }
+ set { SetValue(PeriodProperty, value); }
+ }
+
+ public double HourHeight
+ {
+ get { return (double)GetValue(HourHeightProperty); }
+ set { SetValue(HourHeightProperty, value); }
+ }
+
+ public Thickness EventItemMargin
+ {
+ get { return (Thickness)GetValue(EventItemMarginProperty); }
+ set { SetValue(EventItemMarginProperty, value); }
+ }
+
+ private void ResetMeasurements() => _measurements.Clear();
+
+ private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight)
+ {
+ var childStart = calendarItemViewModel.StartDate;
+
+ if (childStart <= Period.Start)
+ {
+ // Event started before or exactly at the periods tart. This might be a multi-day event.
+ // We can simply consider event must not have a top margin.
+
+ return 0d;
+ }
+
+ double minutesFromStart = (childStart - Period.Start).TotalMinutes;
+ return (minutesFromStart / 1440) * availableHeight;
+ }
+
+ private double GetChildWidth(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
+ {
+ return (calendarItemMeasurement.Right - calendarItemMeasurement.Left) * availableWidth;
+ }
+
+ private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
+ => availableWidth * calendarItemMeasurement.Left;
+
+ private double GetChildHeight(ICalendarItem child)
+ {
+ // All day events are not measured.
+ if (child.IsAllDayEvent) return 0;
+
+ double childDurationInMinutes = 0d;
+ double availableHeight = HourHeight * 24;
+
+ var periodRelation = child.Period.GetRelation(Period);
+
+ // Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}");
+
+ if (!child.IsMultiDayEvent)
+ {
+ childDurationInMinutes = child.Period.Duration.TotalMinutes;
+ }
+ else
+ {
+ // Multi-day event.
+ // Check how many of the event falls into the current period.
+ childDurationInMinutes = (child.Period.End - Period.Start).TotalMinutes;
+ }
+
+ return (childDurationInMinutes / 1440) * availableHeight;
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ ResetMeasurements();
+ return base.MeasureOverride(availableSize);
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ if (Period == null || HourHeight == 0d) return finalSize;
+
+ // Measure/arrange each child height and width.
+ // This is a vertical calendar. Therefore the height of each child is the duration of the event.
+ // Children weights for left and right will be saved if they don't exist.
+ // This is important because we don't want to measure the weights again.
+ // They don't change until new event is added or removed.
+ // Width of the each child may depend on the rectangle packing algorithm.
+ // Children are first categorized into columns. Then each column is shifted to the left until
+ // no overlap occurs. The width of each child is calculated based on the number of columns it spans.
+
+ double availableHeight = finalSize.Height;
+ double availableWidth = finalSize.Width;
+
+ var calendarControls = Children.Cast();
+
+ if (!calendarControls.Any()) return base.ArrangeOverride(finalSize);
+
+ var events = calendarControls.Select(a => a.Content as CalendarItemViewModel);
+
+ LayoutEvents(events);
+
+ foreach (var control in calendarControls)
+ {
+ // We can't arrange this child.
+ if (!(control.Content is ICalendarItem child)) continue;
+
+ bool isHorizontallyLastItem = false;
+
+ double childWidth = 0,
+ childHeight = Math.Max(0, GetChildHeight(child)),
+ childTop = Math.Max(0, GetChildTopMargin(child, availableHeight)),
+ childLeft = 0;
+
+ // No need to measure anything here.
+ if (childHeight == 0) continue;
+
+ if (!_measurements.ContainsKey(child))
+ {
+ // Multi-day event.
+
+ childLeft = 0;
+ childWidth = availableWidth;
+ }
+ else
+ {
+ var childMeasurement = _measurements[child];
+
+ childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width));
+ childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth));
+
+ isHorizontallyLastItem = childMeasurement.Right == 1;
+ }
+
+ // Add additional right margin to items that falls on the right edge of the panel.
+ double extraRightMargin = 0;
+
+ // Multi-day events don't have any margin and their hit test is disabled.
+ if (!child.IsMultiDayEvent)
+ {
+ // Max of 5% of the width or 20px max.
+ extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0;
+ }
+
+ if (childWidth < 0) childWidth = 1;
+
+ // Regular events must have 2px margin
+ if (!child.IsMultiDayEvent && !child.IsAllDayEvent)
+ {
+ childLeft += 2;
+ childTop += 2;
+ childHeight -= 2;
+ childWidth -= 2;
+ }
+
+ var arrangementRect = new Rect(childLeft + EventItemMargin.Left, childTop + EventItemMargin.Top, Math.Max(childWidth - extraRightMargin, 1), childHeight);
+
+ // Make sure measured size will fit in the arranged box.
+ var measureSize = arrangementRect.ToSize();
+ control.Measure(measureSize);
+ control.Arrange(arrangementRect);
+
+ //Debug.WriteLine($"{child.Title}, Measured: {measureSize}, Arranged: {arrangementRect}");
+ }
+
+
+ return finalSize;
+ }
+
+ #region ColumSpanning and Packing Algorithm
+
+ private void AddOrUpdateMeasurement(ICalendarItem calendarItem, CalendarItemMeasurement measurement)
+ {
+ if (_measurements.ContainsKey(calendarItem))
+ {
+ _measurements[calendarItem] = measurement;
+ }
+ else
+ {
+ _measurements.Add(calendarItem, measurement);
+ }
+ }
+
+ // Pick the left and right positions of each event, such that there are no overlap.
+ private void LayoutEvents(IEnumerable events)
+ {
+ var columns = new List>();
+ DateTime? lastEventEnding = null;
+
+ foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate))
+ {
+ // Multi-day events are not measured.
+ if (ev.IsMultiDayEvent) continue;
+
+ if (ev.Period.Start >= lastEventEnding)
+ {
+ PackEvents(columns);
+ columns.Clear();
+ lastEventEnding = null;
+ }
+
+ bool placed = false;
+
+ foreach (var col in columns)
+ {
+ if (!col.Last().Period.OverlapsWith(ev.Period))
+ {
+ col.Add(ev);
+ placed = true;
+ break;
+ }
+ }
+ if (!placed)
+ {
+ columns.Add(new List { ev });
+ }
+ if (lastEventEnding == null || ev.Period.End > lastEventEnding.Value)
+ {
+ lastEventEnding = ev.Period.End;
+ }
+ }
+ if (columns.Count > 0)
+ {
+ PackEvents(columns);
+ }
+ }
+
+ // Set the left and right positions for each event in the connected group.
+ private void PackEvents(List> columns)
+ {
+ float numColumns = columns.Count;
+ int iColumn = 0;
+
+ foreach (var col in columns)
+ {
+ foreach (var ev in col)
+ {
+ int colSpan = ExpandEvent(ev, iColumn, columns);
+
+ var leftWeight = iColumn / numColumns;
+ var rightWeight = (iColumn + colSpan) / numColumns;
+
+ AddOrUpdateMeasurement(ev, new CalendarItemMeasurement(leftWeight, rightWeight));
+ }
+
+ iColumn++;
+ }
+ }
+
+ // Checks how many columns the event can expand into, without colliding with other events.
+ private int ExpandEvent(ICalendarItem ev, int iColumn, List> columns)
+ {
+ int colSpan = 1;
+
+ foreach (var col in columns.Skip(iColumn + 1))
+ {
+ foreach (var ev1 in col)
+ {
+ if (ev1.Period.OverlapsWith(ev.Period)) return colSpan;
+ }
+
+ colSpan++;
+ }
+
+ return colSpan;
+ }
+
+ #endregion
+}
diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarTypeSelectorControl.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarTypeSelectorControl.cs
new file mode 100644
index 00000000..e2fbfa82
--- /dev/null
+++ b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarTypeSelectorControl.cs
@@ -0,0 +1,91 @@
+using System.Windows.Input;
+using CommunityToolkit.Diagnostics;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Wino.Core.Domain.Enums;
+
+namespace Wino.Calendar.Controls;
+
+public partial class WinoCalendarTypeSelectorControl : Control
+{
+ private const string PART_TodayButton = nameof(PART_TodayButton);
+ private const string PART_DayToggle = nameof(PART_DayToggle);
+ private const string PART_WeekToggle = nameof(PART_WeekToggle);
+ private const string PART_MonthToggle = nameof(PART_MonthToggle);
+ private const string PART_YearToggle = nameof(PART_YearToggle);
+
+ public static readonly DependencyProperty SelectedTypeProperty = DependencyProperty.Register(nameof(SelectedType), typeof(CalendarDisplayType), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(CalendarDisplayType.Week));
+ public static readonly DependencyProperty DisplayDayCountProperty = DependencyProperty.Register(nameof(DisplayDayCount), typeof(int), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(0));
+ public static readonly DependencyProperty TodayClickedCommandProperty = DependencyProperty.Register(nameof(TodayClickedCommand), typeof(ICommand), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(null));
+
+ public ICommand TodayClickedCommand
+ {
+ get { return (ICommand)GetValue(TodayClickedCommandProperty); }
+ set { SetValue(TodayClickedCommandProperty, value); }
+ }
+
+ public CalendarDisplayType SelectedType
+ {
+ get { return (CalendarDisplayType)GetValue(SelectedTypeProperty); }
+ set { SetValue(SelectedTypeProperty, value); }
+ }
+
+ public int DisplayDayCount
+ {
+ get { return (int)GetValue(DisplayDayCountProperty); }
+ set { SetValue(DisplayDayCountProperty, value); }
+ }
+
+ private AppBarButton _todayButton;
+ private AppBarToggleButton _dayToggle;
+ private AppBarToggleButton _weekToggle;
+ private AppBarToggleButton _monthToggle;
+ private AppBarToggleButton _yearToggle;
+
+ public WinoCalendarTypeSelectorControl()
+ {
+ DefaultStyleKey = typeof(WinoCalendarTypeSelectorControl);
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ _todayButton = GetTemplateChild(PART_TodayButton) as AppBarButton;
+ _dayToggle = GetTemplateChild(PART_DayToggle) as AppBarToggleButton;
+ _weekToggle = GetTemplateChild(PART_WeekToggle) as AppBarToggleButton;
+ _monthToggle = GetTemplateChild(PART_MonthToggle) as AppBarToggleButton;
+ _yearToggle = GetTemplateChild(PART_YearToggle) as AppBarToggleButton;
+
+ Guard.IsNotNull(_todayButton, nameof(_todayButton));
+ Guard.IsNotNull(_dayToggle, nameof(_dayToggle));
+ Guard.IsNotNull(_weekToggle, nameof(_weekToggle));
+ Guard.IsNotNull(_monthToggle, nameof(_monthToggle));
+ Guard.IsNotNull(_yearToggle, nameof(_yearToggle));
+
+ _todayButton.Click += TodayClicked;
+
+ _dayToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Day); };
+ _weekToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Week); };
+ _monthToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Month); };
+ _yearToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Year); };
+
+ UpdateToggleButtonStates();
+ }
+
+ private void TodayClicked(object sender, RoutedEventArgs e) => TodayClickedCommand?.Execute(null);
+
+ private void SetSelectedType(CalendarDisplayType type)
+ {
+ SelectedType = type;
+ UpdateToggleButtonStates();
+ }
+
+ private void UpdateToggleButtonStates()
+ {
+ _dayToggle.IsChecked = SelectedType == CalendarDisplayType.Day;
+ _weekToggle.IsChecked = SelectedType == CalendarDisplayType.Week;
+ _monthToggle.IsChecked = SelectedType == CalendarDisplayType.Month;
+ _yearToggle.IsChecked = SelectedType == CalendarDisplayType.Year;
+ }
+}
diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarView.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarView.cs
new file mode 100644
index 00000000..ea2f3c66
--- /dev/null
+++ b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarView.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Windows.Input;
+using CommunityToolkit.Diagnostics;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+using Windows.UI;
+using Wino.Core.Domain.Models.Calendar;
+using Wino.Helpers;
+
+namespace Wino.Calendar.Controls;
+
+public partial class WinoCalendarView : Control
+{
+ private const string PART_DayViewItemBorder = nameof(PART_DayViewItemBorder);
+ private const string PART_CalendarView = nameof(PART_CalendarView);
+
+ public static readonly DependencyProperty HighlightedDateRangeProperty = DependencyProperty.Register(nameof(HighlightedDateRange), typeof(DateRange), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnHighlightedDateRangeChanged)));
+ public static readonly DependencyProperty VisibleDateBackgroundProperty = DependencyProperty.Register(nameof(VisibleDateBackground), typeof(Brush), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnPropertiesChanged)));
+ public static readonly DependencyProperty DateClickedCommandProperty = DependencyProperty.Register(nameof(DateClickedCommand), typeof(ICommand), typeof(WinoCalendarView), new PropertyMetadata(null));
+ public static readonly DependencyProperty TodayBackgroundColorProperty = DependencyProperty.Register(nameof(TodayBackgroundColor), typeof(Color), typeof(WinoCalendarView), new PropertyMetadata(null));
+
+ public Color TodayBackgroundColor
+ {
+ get { return (Color)GetValue(TodayBackgroundColorProperty); }
+ set { SetValue(TodayBackgroundColorProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the command to execute when a date is picked.
+ /// Unused.
+ ///
+ public ICommand DateClickedCommand
+ {
+ get { return (ICommand)GetValue(DateClickedCommandProperty); }
+ set { SetValue(DateClickedCommandProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the highlighted range of dates.
+ ///
+ public DateRange HighlightedDateRange
+ {
+ get { return (DateRange)GetValue(HighlightedDateRangeProperty); }
+ set { SetValue(HighlightedDateRangeProperty, value); }
+ }
+
+ public Brush VisibleDateBackground
+ {
+ get { return (Brush)GetValue(VisibleDateBackgroundProperty); }
+ set { SetValue(VisibleDateBackgroundProperty, value); }
+ }
+
+
+
+ private CalendarView CalendarView;
+
+ public WinoCalendarView()
+ {
+ DefaultStyleKey = typeof(WinoCalendarView);
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ CalendarView = GetTemplateChild(PART_CalendarView) as CalendarView;
+
+ Guard.IsNotNull(CalendarView, nameof(CalendarView));
+
+ CalendarView.SelectedDatesChanged -= InternalCalendarViewSelectionChanged;
+ CalendarView.SelectedDatesChanged += InternalCalendarViewSelectionChanged;
+
+ // TODO: Should come from settings.
+ CalendarView.FirstDayOfWeek = Windows.Globalization.DayOfWeek.Monday;
+
+ // Everytime display mode changes, update the visible date range backgrounds.
+ // If users go back from year -> month -> day, we need to update the visible date range backgrounds.
+
+ CalendarView.RegisterPropertyChangedCallback(CalendarView.DisplayModeProperty, (s, e) => UpdateVisibleDateRangeBackgrounds());
+ }
+
+ private void InternalCalendarViewSelectionChanged(CalendarView sender, CalendarViewSelectedDatesChangedEventArgs args)
+ {
+ if (args.AddedDates?.Count > 0)
+ {
+ var clickedDate = args.AddedDates[0].Date;
+ SetInnerDisplayDate(clickedDate);
+
+ var clickArgs = new CalendarViewDayClickedEventArgs(clickedDate);
+ DateClickedCommand?.Execute(clickArgs);
+ }
+
+ // Reset selection, we don't show selected dates but react to them.
+ CalendarView.SelectedDates.Clear();
+ }
+
+ private static void OnPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is WinoCalendarView control)
+ {
+ control.UpdateVisibleDateRangeBackgrounds();
+ }
+ }
+
+ private void SetInnerDisplayDate(DateTime dateTime) => CalendarView?.SetDisplayDate(dateTime);
+
+ // Changing selected dates will trigger the selection changed event.
+ // It will behave like user clicked the date.
+ public void GoToDay(DateTime dateTime) => CalendarView.SelectedDates.Add(dateTime);
+
+ private static void OnHighlightedDateRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is WinoCalendarView control)
+ {
+ control.SetInnerDisplayDate(control.HighlightedDateRange.StartDate);
+ control.UpdateVisibleDateRangeBackgrounds();
+ }
+ }
+
+ public void UpdateVisibleDateRangeBackgrounds()
+ {
+ if (HighlightedDateRange == null || VisibleDateBackground == null || TodayBackgroundColor == null || CalendarView == null) return;
+
+ var markDateCalendarDayItems = WinoVisualTreeHelper.FindDescendants(CalendarView);
+
+ foreach (var calendarDayItem in markDateCalendarDayItems)
+ {
+ var border = WinoVisualTreeHelper.GetChildObject(calendarDayItem, PART_DayViewItemBorder);
+
+ if (border == null) return;
+
+ if (calendarDayItem.Date.Date == DateTime.Today.Date)
+ {
+ border.Background = new SolidColorBrush(TodayBackgroundColor);
+ }
+ else if (calendarDayItem.Date.Date >= HighlightedDateRange.StartDate.Date && calendarDayItem.Date.Date < HighlightedDateRange.EndDate.Date)
+ {
+ border.Background = VisibleDateBackground;
+ }
+ else
+ {
+ border.Background = null;
+ }
+ }
+ }
+}
diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoDayTimelineCanvas.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoDayTimelineCanvas.cs
new file mode 100644
index 00000000..7ba154e5
--- /dev/null
+++ b/Wino.Mail.WinUI/Controls/Calendar/WinoDayTimelineCanvas.cs
@@ -0,0 +1,278 @@
+using System;
+using System.Diagnostics;
+using Microsoft.Graphics.Canvas.Geometry;
+using Microsoft.Graphics.Canvas.UI.Xaml;
+using Microsoft.UI.Input;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
+using Windows.Foundation;
+using Wino.Calendar.Args;
+using Wino.Core.Domain.Models.Calendar;
+
+namespace Wino.Calendar.Controls;
+
+public partial class WinoDayTimelineCanvas : Control, IDisposable
+{
+ public event EventHandler TimelineCellSelected;
+ public event EventHandler TimelineCellUnselected;
+
+ private const string PART_InternalCanvas = nameof(PART_InternalCanvas);
+ private CanvasControl Canvas;
+
+ public static readonly DependencyProperty RenderOptionsProperty = DependencyProperty.Register(nameof(RenderOptions), typeof(CalendarRenderOptions), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
+ public static readonly DependencyProperty SeperatorColorProperty = DependencyProperty.Register(nameof(SeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
+ public static readonly DependencyProperty HalfHourSeperatorColorProperty = DependencyProperty.Register(nameof(HalfHourSeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
+ public static readonly DependencyProperty SelectedCellBackgroundBrushProperty = DependencyProperty.Register(nameof(SelectedCellBackgroundBrush), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
+ public static readonly DependencyProperty WorkingHourCellBackgroundColorProperty = DependencyProperty.Register(nameof(WorkingHourCellBackgroundColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
+ public static readonly DependencyProperty SelectedDateTimeProperty = DependencyProperty.Register(nameof(SelectedDateTime), typeof(DateTime?), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedDateTimeChanged)));
+ public static readonly DependencyProperty PositionerUIElementProperty = DependencyProperty.Register(nameof(PositionerUIElement), typeof(UIElement), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null));
+
+ public UIElement PositionerUIElement
+ {
+ get { return (UIElement)GetValue(PositionerUIElementProperty); }
+ set { SetValue(PositionerUIElementProperty, value); }
+ }
+
+ public CalendarRenderOptions RenderOptions
+ {
+ get { return (CalendarRenderOptions)GetValue(RenderOptionsProperty); }
+ set { SetValue(RenderOptionsProperty, value); }
+ }
+
+ public SolidColorBrush HalfHourSeperatorColor
+ {
+ get { return (SolidColorBrush)GetValue(HalfHourSeperatorColorProperty); }
+ set { SetValue(HalfHourSeperatorColorProperty, value); }
+ }
+
+ public SolidColorBrush SeperatorColor
+ {
+ get { return (SolidColorBrush)GetValue(SeperatorColorProperty); }
+ set { SetValue(SeperatorColorProperty, value); }
+ }
+
+ public SolidColorBrush WorkingHourCellBackgroundColor
+ {
+ get { return (SolidColorBrush)GetValue(WorkingHourCellBackgroundColorProperty); }
+ set { SetValue(WorkingHourCellBackgroundColorProperty, value); }
+ }
+
+ public SolidColorBrush SelectedCellBackgroundBrush
+ {
+ get { return (SolidColorBrush)GetValue(SelectedCellBackgroundBrushProperty); }
+ set { SetValue(SelectedCellBackgroundBrushProperty, value); }
+ }
+
+ public DateTime? SelectedDateTime
+ {
+ get { return (DateTime?)GetValue(SelectedDateTimeProperty); }
+ set { SetValue(SelectedDateTimeProperty, value); }
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ Canvas = GetTemplateChild(PART_InternalCanvas) as CanvasControl;
+
+ // TODO: These will leak. Dispose them properly when needed.
+ Canvas.Draw += OnCanvasDraw;
+ Canvas.PointerPressed += OnCanvasPointerPressed;
+
+ ForceDraw();
+ }
+
+ private static void OnSelectedDateTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is WinoDayTimelineCanvas control)
+ {
+ if (e.OldValue != null && e.NewValue == null)
+ {
+ control.RaiseCellUnselected();
+ }
+
+ control.ForceDraw();
+ }
+ }
+
+ private void RaiseCellUnselected()
+ {
+ TimelineCellUnselected?.Invoke(this, new TimelineCellUnselectedArgs());
+ }
+
+ private void OnCanvasPointerPressed(object sender, PointerRoutedEventArgs e)
+ {
+ if (RenderOptions == null) return;
+
+ var hourHeight = RenderOptions.CalendarSettings.HourHeight;
+
+ // 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.
+
+ PointerPoint positionerRootPoint = e.GetCurrentPoint(PositionerUIElement);
+ PointerPoint canvasPointerPoint = e.GetCurrentPoint(Canvas);
+
+ Point touchPoint = canvasPointerPoint.Position;
+
+ var singleDayWidth = (Canvas.ActualWidth / RenderOptions.TotalDayCount);
+
+ int day = (int)(touchPoint.X / singleDayWidth);
+ int hour = (int)(touchPoint.Y / hourHeight);
+
+ bool isSecondHalf = touchPoint.Y % hourHeight > (hourHeight / 2);
+
+ var diffX = positionerRootPoint.Position.X - touchPoint.X;
+ var diffY = positionerRootPoint.Position.Y - touchPoint.Y;
+
+ var cellStartRelativePositionX = diffX + (day * singleDayWidth);
+ var cellEndRelativePositionX = cellStartRelativePositionX + singleDayWidth;
+
+ var cellStartRelativePositionY = diffY + (hour * hourHeight) + (isSecondHalf ? hourHeight / 2 : 0);
+ var cellEndRelativePositionY = cellStartRelativePositionY + (isSecondHalf ? (hourHeight / 2) : hourHeight);
+
+ var cellSize = new Size(cellEndRelativePositionX - cellStartRelativePositionX, hourHeight / 2);
+ var positionerPoint = new Point(cellStartRelativePositionX, cellStartRelativePositionY);
+
+ var clickedDateTime = RenderOptions.DateRange.StartDate.AddDays(day).AddHours(hour).AddMinutes(isSecondHalf ? 30 : 0);
+
+ // If there is already a selected date, in order to mimic the popup behavior, we need to dismiss the previous selection first.
+ // Next click will be a new selection.
+
+ // Raise the events directly here instead of DP to not lose pointer position.
+ if (clickedDateTime == SelectedDateTime || SelectedDateTime != null)
+ {
+ SelectedDateTime = null;
+ }
+ else
+ {
+ SelectedDateTime = clickedDateTime;
+ TimelineCellSelected?.Invoke(this, new TimelineCellSelectedArgs(clickedDateTime, touchPoint, positionerPoint, cellSize));
+ }
+
+ Debug.WriteLine($"Clicked: {clickedDateTime}");
+ }
+
+ public WinoDayTimelineCanvas()
+ {
+ DefaultStyleKey = typeof(WinoDayTimelineCanvas);
+ }
+
+ private static void OnRenderingPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is WinoDayTimelineCanvas control)
+ {
+ control.ForceDraw();
+ }
+ }
+
+ private void ForceDraw() => Canvas?.Invalidate();
+
+ private bool CanDrawTimeline()
+ {
+ return RenderOptions != null
+ && Canvas != null
+ && Canvas.ReadyToDraw
+ && WorkingHourCellBackgroundColor != null
+ && SeperatorColor != null
+ && HalfHourSeperatorColor != null
+ && SelectedCellBackgroundBrush != null;
+ }
+
+ private void OnCanvasDraw(CanvasControl sender, CanvasDrawEventArgs args)
+ {
+ if (!CanDrawTimeline()) return;
+
+ int hours = 24;
+
+ double canvasWidth = Canvas.ActualWidth;
+ double canvasHeight = Canvas.ActualHeight;
+
+ if (canvasWidth == 0 || canvasHeight == 0) return;
+
+ // Calculate the width of each rectangle (1 day column)
+ // Equal distribution of the whole width.
+ double rectWidth = canvasWidth / RenderOptions.TotalDayCount;
+
+ // Calculate the height of each rectangle (1 hour row)
+ double rectHeight = RenderOptions.CalendarSettings.HourHeight;
+
+ // Define stroke and fill colors
+ var strokeColor = SeperatorColor.Color;
+ float strokeThickness = 0.5f;
+
+ for (int day = 0; day < RenderOptions.TotalDayCount; day++)
+ {
+ var currentDay = RenderOptions.DateRange.StartDate.AddDays(day);
+
+ bool isWorkingDay = RenderOptions.CalendarSettings.WorkingDays.Contains(currentDay.DayOfWeek);
+
+ // Loop through each hour (rows)
+ for (int hour = 0; hour < hours; hour++)
+ {
+ var renderTime = TimeSpan.FromHours(hour);
+
+ var representingDateTime = currentDay.AddHours(hour);
+
+ // Calculate the position and size of the rectangle
+ double x = day * rectWidth;
+ double y = hour * rectHeight;
+
+ var rectangle = new Rect(x, y, rectWidth, rectHeight);
+
+ // Draw the rectangle border.
+ // This is the main rectangle.
+ args.DrawingSession.DrawRectangle(rectangle, strokeColor, strokeThickness);
+
+ // Fill another rectangle with the working hour background color
+ // This rectangle must be placed with -1 margin to prevent invisible borders of the main rectangle.
+ if (isWorkingDay && renderTime >= RenderOptions.CalendarSettings.WorkingHourStart && renderTime <= RenderOptions.CalendarSettings.WorkingHourEnd)
+ {
+ var backgroundRectangle = new Rect(x + 1, y + 1, rectWidth - 1, rectHeight - 1);
+
+ args.DrawingSession.DrawRectangle(backgroundRectangle, strokeColor, strokeThickness);
+ args.DrawingSession.FillRectangle(backgroundRectangle, WorkingHourCellBackgroundColor.Color);
+ }
+
+ // Draw a line in the center of the rectangle for representing half hours.
+ double lineY = y + rectHeight / 2;
+
+ args.DrawingSession.DrawLine((float)x, (float)lineY, (float)(x + rectWidth), (float)lineY, HalfHourSeperatorColor.Color, strokeThickness, new CanvasStrokeStyle()
+ {
+ DashStyle = CanvasDashStyle.Dot
+ });
+ }
+
+ // Draw selected item background color for the date if possible.
+ if (SelectedDateTime != null)
+ {
+ var selectedDateTime = SelectedDateTime.Value;
+ if (selectedDateTime.Date == currentDay.Date)
+ {
+ var selectionRectHeight = rectHeight / 2;
+ var selectedY = selectedDateTime.Hour * rectHeight + (selectedDateTime.Minute / 60) * rectHeight;
+
+ // Second half of the hour is selected.
+ if (selectedDateTime.TimeOfDay.Minutes == 30)
+ {
+ selectedY += rectHeight / 2;
+ }
+
+ var selectedRectangle = new Rect(day * rectWidth, selectedY, rectWidth, selectionRectHeight);
+ args.DrawingSession.FillRectangle(selectedRectangle, SelectedCellBackgroundBrush.Color);
+ }
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (Canvas == null) return;
+
+ Canvas.Draw -= OnCanvasDraw;
+ Canvas.PointerPressed -= OnCanvasPointerPressed;
+ Canvas.RemoveFromVisualTree();
+
+ Canvas = null;
+ }
+}
diff --git a/Wino.Mail.WinUI/Helpers/CalendarXamlHelpers.cs b/Wino.Mail.WinUI/Helpers/CalendarXamlHelpers.cs
new file mode 100644
index 00000000..86267f58
--- /dev/null
+++ b/Wino.Mail.WinUI/Helpers/CalendarXamlHelpers.cs
@@ -0,0 +1,106 @@
+using System.Linq;
+using System.Text.RegularExpressions;
+using Ical.Net.CalendarComponents;
+using Ical.Net.DataTypes;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Wino.Calendar.ViewModels.Data;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Collections;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Calendar;
+using Wino.Helpers;
+
+namespace Wino.Calendar.Helpers;
+
+public static class CalendarXamlHelpers
+{
+ public static CalendarItemViewModel GetFirstAllDayEvent(CalendarEventCollection collection)
+ => (CalendarItemViewModel)collection.AllDayEvents.FirstOrDefault();
+
+ ///
+ /// Returns full date + duration info in Event Details page details title.
+ ///
+ public static string GetEventDetailsDateString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
+ {
+ if (calendarItemViewModel == null || settings == null) return string.Empty;
+
+ var start = calendarItemViewModel.Period.Start;
+ var end = calendarItemViewModel.Period.End;
+
+ string timeFormat = settings.DayHeaderDisplayType == DayHeaderDisplayType.TwelveHour ? "h:mm tt" : "HH:mm";
+ string dateFormat = settings.DayHeaderDisplayType == DayHeaderDisplayType.TwelveHour ? "dddd, dd MMMM h:mm tt" : "dddd, dd MMMM HH:mm";
+
+ if (calendarItemViewModel.IsMultiDayEvent)
+ {
+ return $"{start.ToString($"dd MMMM ddd {timeFormat}", settings.CultureInfo)} - {end.ToString($"dd MMMM ddd {timeFormat}", settings.CultureInfo)}";
+ }
+ else
+ {
+ return $"{start.ToString(dateFormat, settings.CultureInfo)} - {end.ToString(timeFormat, settings.CultureInfo)}";
+ }
+ }
+
+ public static string GetRecurrenceString(CalendarItemViewModel calendarItemViewModel)
+ {
+ if (calendarItemViewModel == null || !calendarItemViewModel.IsRecurringChild) return string.Empty;
+
+ // Parse recurrence rules
+ var calendarEvent = new CalendarEvent
+ {
+ Start = new CalDateTime(calendarItemViewModel.StartDate),
+ End = new CalDateTime(calendarItemViewModel.EndDate),
+ };
+
+ var recurrenceLines = Regex.Split(calendarItemViewModel.CalendarItem.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
+
+ foreach (var line in recurrenceLines)
+ {
+ calendarEvent.RecurrenceRules.Add(new RecurrencePattern(line));
+ }
+
+ if (calendarEvent.RecurrenceRules == null || !calendarEvent.RecurrenceRules.Any())
+ {
+ return "No recurrence pattern.";
+ }
+
+ var recurrenceRule = calendarEvent.RecurrenceRules.First();
+ var daysOfWeek = string.Join(", ", recurrenceRule.ByDay.Select(day => day.DayOfWeek.ToString()));
+ string timeZone = calendarEvent.DtStart.TzId ?? "UTC";
+
+ return $"Every {daysOfWeek}, effective {calendarEvent.DtStart.Value.ToShortDateString()} " +
+ $"from {calendarEvent.DtStart.Value.ToShortTimeString()} to {calendarEvent.DtEnd.Value.ToShortTimeString()} " +
+ $"{timeZone}.";
+ }
+
+ public static string GetDetailsPopupDurationString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
+ {
+ if (calendarItemViewModel == null || settings == null) return string.Empty;
+
+ // Single event in a day.
+ if (!calendarItemViewModel.IsAllDayEvent && !calendarItemViewModel.IsMultiDayEvent)
+ {
+ return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} {settings.GetTimeString(calendarItemViewModel.Period.Duration)}";
+ }
+ else if (calendarItemViewModel.IsMultiDayEvent)
+ {
+ return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} - {calendarItemViewModel.Period.End.ToString("d", settings.CultureInfo)}";
+ }
+ else
+ {
+ // All day event.
+ return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} ({Translator.CalendarItemAllDay})";
+ }
+ }
+
+ public static PopupPlacementMode GetDesiredPlacementModeForEventsDetailsPopup(
+ CalendarItemViewModel calendarItemViewModel,
+ CalendarDisplayType calendarDisplayType)
+ {
+ if (calendarItemViewModel == null) return PopupPlacementMode.Auto;
+
+ // All and/or multi day events always go to the top of the screen.
+ if (calendarItemViewModel.IsAllDayEvent || calendarItemViewModel.IsMultiDayEvent) return PopupPlacementMode.Bottom;
+
+ return XamlHelpers.GetPlaccementModeForCalendarType(calendarDisplayType);
+ }
+}
diff --git a/Wino.Mail.WinUI/AppShell.xaml b/Wino.Mail.WinUI/MailAppShell.xaml
similarity index 99%
rename from Wino.Mail.WinUI/AppShell.xaml
rename to Wino.Mail.WinUI/MailAppShell.xaml
index 176f55d3..4eda9d55 100644
--- a/Wino.Mail.WinUI/AppShell.xaml
+++ b/Wino.Mail.WinUI/MailAppShell.xaml
@@ -1,5 +1,5 @@
-
@@ -475,4 +475,4 @@
-
+
diff --git a/Wino.Mail.WinUI/AppShell.xaml.cs b/Wino.Mail.WinUI/MailAppShell.xaml.cs
similarity index 91%
rename from Wino.Mail.WinUI/AppShell.xaml.cs
rename to Wino.Mail.WinUI/MailAppShell.xaml.cs
index 85197069..7b3e992d 100644
--- a/Wino.Mail.WinUI/AppShell.xaml.cs
+++ b/Wino.Mail.WinUI/MailAppShell.xaml.cs
@@ -9,7 +9,6 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
-using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Mail;
@@ -18,10 +17,10 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation;
-using Wino.Mail.WinUI;
-using Wino.Mail.WinUI.Controls;
using Wino.Extensions;
using Wino.Mail.ViewModels.Data;
+using Wino.Mail.WinUI;
+using Wino.Mail.WinUI.Controls;
using Wino.MenuFlyouts;
using Wino.MenuFlyouts.Context;
using Wino.Messaging.Client.Accounts;
@@ -32,7 +31,7 @@ using Wino.Views.Abstract;
namespace Wino.Views;
-public sealed partial class AppShell : AppShellAbstract,
+public sealed partial class MailAppShell : MailAppShellAbstract,
IRecipient,
IRecipient,
IRecipient,
@@ -41,26 +40,11 @@ public sealed partial class AppShell : AppShellAbstract,
[GeneratedDependencyProperty]
public partial UIElement? TopShellContent { get; set; }
- public AppShell() : base()
+ public MailAppShell() : base()
{
InitializeComponent();
}
- //protected override void OnNavigatedTo(NavigationEventArgs e)
- //{
- // base.OnNavigatedTo(e);
-
- // WeakReferenceMessenger.Default.Register(this);
- // WeakReferenceMessenger.Default.Register(this);
- // WeakReferenceMessenger.Default.Register(this);
- // WeakReferenceMessenger.Default.Register(this);
- //}
-
- protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
- {
- base.OnNavigatingFrom(e);
- }
-
private async void ItemDroppedOnFolder(object sender, DragEventArgs e)
{
// Validate package content.
@@ -224,7 +208,7 @@ public sealed partial class AppShell : AppShellAbstract,
{
var navigateFolderArgs = new NavigateMailFolderEventArgs(message.BaseFolderMenuItem, message.FolderInitLoadAwaitTask);
- ViewModel.NavigationService.Navigate(WinoPage.MailListPage, navigateFolderArgs, NavigationReferenceFrame.ShellFrame);
+ ViewModel.NavigationService.Navigate(WinoPage.MailListPage, navigateFolderArgs, NavigationReferenceFrame.InnerShellFrame);
// Prevent double navigation.
navigationView.SelectionChanged -= MenuSelectionChanged;
@@ -330,15 +314,15 @@ public sealed partial class AppShell : AppShellAbstract,
});
}
- private void NavigationViewDisplayModeChanged(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewDisplayModeChangedEventArgs args)
+ private void NavigationViewDisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
{
- if (args.DisplayMode == Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode.Minimal)
+ if (args.DisplayMode == NavigationViewDisplayMode.Minimal)
{
- ShellFrame.Margin = new Thickness(7, 0, 0, 0);
+ InnerShellFrame.Margin = new Thickness(7, 0, 0, 0);
}
else
{
- ShellFrame.Margin = new Thickness(0);
+ InnerShellFrame.Margin = new Thickness(0);
}
}
diff --git a/Wino.Mail.WinUI/MainWindow.xaml b/Wino.Mail.WinUI/MainWindow.xaml
deleted file mode 100644
index 126da732..00000000
--- a/Wino.Mail.WinUI/MainWindow.xaml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/Wino.Mail.WinUI/MainWindow.xaml.cs b/Wino.Mail.WinUI/MainWindow.xaml.cs
deleted file mode 100644
index 88396eee..00000000
--- a/Wino.Mail.WinUI/MainWindow.xaml.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices.WindowsRuntime;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
-using Microsoft.UI.Xaml.Controls.Primitives;
-using Microsoft.UI.Xaml.Data;
-using Microsoft.UI.Xaml.Input;
-using Microsoft.UI.Xaml.Media;
-using Microsoft.UI.Xaml.Navigation;
-using Windows.Foundation;
-using Windows.Foundation.Collections;
-
-// To learn more about WinUI, the WinUI project structure,
-// and more about our project templates, see: http://aka.ms/winui-project-info.
-
-namespace Wino.Mail.WinUI;
-///
-/// An empty window that can be used on its own or navigated to within a Frame.
-///
-public sealed partial class MainWindow : Window
-{
- public MainWindow()
- {
- InitializeComponent();
- }
-}
diff --git a/Wino.Mail.WinUI/Models/Args/TimelineCellSelectedArgs.cs b/Wino.Mail.WinUI/Models/Args/TimelineCellSelectedArgs.cs
new file mode 100644
index 00000000..481e0182
--- /dev/null
+++ b/Wino.Mail.WinUI/Models/Args/TimelineCellSelectedArgs.cs
@@ -0,0 +1,40 @@
+using System;
+using Windows.Foundation;
+
+namespace Wino.Calendar.Args;
+
+///
+/// When a new timeline cell is selected.
+///
+public class TimelineCellSelectedArgs : EventArgs
+{
+ public TimelineCellSelectedArgs(DateTime clickedDate, Point canvasPoint, Point positionerPoint, Size cellSize)
+ {
+ ClickedDate = clickedDate;
+ CanvasPoint = canvasPoint;
+ PositionerPoint = positionerPoint;
+ CellSize = cellSize;
+ }
+
+ ///
+ /// Clicked date and time information for the cell.
+ ///
+ public DateTime ClickedDate { get; set; }
+
+ ///
+ /// Position relative to the cell drawing part of the canvas.
+ /// Used to detect clicked cell from the position.
+ ///
+ public Point CanvasPoint { get; }
+
+ ///
+ /// Position relative to the main root positioner element of the drawing canvas.
+ /// Used to show the create event dialog teaching tip in correct position.
+ ///
+ public Point PositionerPoint { get; }
+
+ ///
+ /// Size of the cell.
+ ///
+ public Size CellSize { get; }
+}
diff --git a/Wino.Mail.WinUI/Models/Args/TimelineCellUnselectedArgs.cs b/Wino.Mail.WinUI/Models/Args/TimelineCellUnselectedArgs.cs
new file mode 100644
index 00000000..7cbeb841
--- /dev/null
+++ b/Wino.Mail.WinUI/Models/Args/TimelineCellUnselectedArgs.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace Wino.Calendar.Args;
+
+///
+/// When selected timeline cell is unselected.
+///
+public class TimelineCellUnselectedArgs : EventArgs { }
diff --git a/Wino.Mail.WinUI/Models/CalendarItemMeasurement.cs b/Wino.Mail.WinUI/Models/CalendarItemMeasurement.cs
new file mode 100644
index 00000000..dbb8cf9b
--- /dev/null
+++ b/Wino.Mail.WinUI/Models/CalendarItemMeasurement.cs
@@ -0,0 +1,16 @@
+namespace Wino.Calendar.Models;
+
+public struct CalendarItemMeasurement
+{
+ // Where to start?
+ public double Left { get; set; }
+
+ // Extend until where?
+ public double Right { get; set; }
+
+ public CalendarItemMeasurement(double left, double right)
+ {
+ Left = left;
+ Right = right;
+ }
+}
diff --git a/Wino.Mail.WinUI/Selectors/CustomAreaCalendarItemSelector.cs b/Wino.Mail.WinUI/Selectors/CustomAreaCalendarItemSelector.cs
new file mode 100644
index 00000000..35996d01
--- /dev/null
+++ b/Wino.Mail.WinUI/Selectors/CustomAreaCalendarItemSelector.cs
@@ -0,0 +1,21 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Wino.Calendar.ViewModels.Data;
+
+namespace Wino.Calendar.Selectors;
+
+public partial class CustomAreaCalendarItemSelector : DataTemplateSelector
+{
+ public DataTemplate AllDayTemplate { get; set; }
+ public DataTemplate MultiDayTemplate { get; set; }
+
+ protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
+ {
+ if (item is CalendarItemViewModel calendarItemViewModel)
+ {
+ return calendarItemViewModel.IsMultiDayEvent ? MultiDayTemplate : AllDayTemplate;
+ }
+
+ return base.SelectTemplateCore(item, container);
+ }
+}
diff --git a/Wino.Mail.WinUI/Selectors/WinoCalendarItemTemplateSelector.cs b/Wino.Mail.WinUI/Selectors/WinoCalendarItemTemplateSelector.cs
new file mode 100644
index 00000000..2d3ec477
--- /dev/null
+++ b/Wino.Mail.WinUI/Selectors/WinoCalendarItemTemplateSelector.cs
@@ -0,0 +1,33 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Wino.Core.Domain.Enums;
+
+namespace Wino.Selectors;
+
+public partial class WinoCalendarItemTemplateSelector : DataTemplateSelector
+{
+ public CalendarDisplayType DisplayType { get; set; }
+
+ public DataTemplate DayWeekWorkWeekTemplate { get; set; }
+ public DataTemplate MonthlyTemplate { get; set; }
+
+
+ protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
+ {
+ switch (DisplayType)
+ {
+ case CalendarDisplayType.Day:
+ case CalendarDisplayType.Week:
+ case CalendarDisplayType.WorkWeek:
+ return DayWeekWorkWeekTemplate;
+ case CalendarDisplayType.Month:
+ return MonthlyTemplate;
+ case CalendarDisplayType.Year:
+ break;
+ default:
+ break;
+ }
+
+ return base.SelectTemplateCore(item, container);
+ }
+}
diff --git a/Wino.Mail.WinUI/Services/AccountCalendarStateService.cs b/Wino.Mail.WinUI/Services/AccountCalendarStateService.cs
new file mode 100644
index 00000000..a0bb5212
--- /dev/null
+++ b/Wino.Mail.WinUI/Services/AccountCalendarStateService.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+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;
+
+///
+/// Encapsulated state manager for collectively managing the state of account calendars.
+/// Callers must react to the events to update their state only from this service.
+///
+public partial class AccountCalendarStateService : ObservableObject, IAccountCalendarStateService
+{
+ public event EventHandler CollectiveAccountGroupSelectionStateChanged;
+ public event EventHandler AccountCalendarSelectionStateChanged;
+
+ [ObservableProperty]
+ public partial ReadOnlyObservableCollection GroupedAccountCalendars { get; set; }
+
+ private ObservableCollection _internalGroupedAccountCalendars = new ObservableCollection();
+
+ public IEnumerable ActiveCalendars
+ {
+ get
+ {
+ return GroupedAccountCalendars
+ .SelectMany(a => a.AccountCalendars)
+ .Where(b => b.IsChecked);
+ }
+ }
+
+ public IEnumerable> GroupedAccountCalendarsEnumerable
+ {
+ get
+ {
+ return GroupedAccountCalendars
+ .Select(a => a.AccountCalendars)
+ .SelectMany(b => b)
+ .GroupBy(c => c.Account);
+ }
+ }
+
+ public AccountCalendarStateService()
+ {
+ GroupedAccountCalendars = new ReadOnlyObservableCollection(_internalGroupedAccountCalendars);
+ }
+
+ private void SingleGroupCalendarCollectiveStateChanged(object sender, EventArgs e)
+ => CollectiveAccountGroupSelectionStateChanged?.Invoke(this, sender as GroupedAccountCalendarViewModel);
+
+ private void SingleCalendarSelectionStateChanged(object sender, AccountCalendarViewModel e)
+ => AccountCalendarSelectionStateChanged?.Invoke(this, e);
+
+ public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar)
+ {
+ groupedAccountCalendar.CalendarSelectionStateChanged += SingleCalendarSelectionStateChanged;
+ groupedAccountCalendar.CollectiveSelectionStateChanged += SingleGroupCalendarCollectiveStateChanged;
+
+ _internalGroupedAccountCalendars.Add(groupedAccountCalendar);
+ }
+
+ public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar)
+ {
+ groupedAccountCalendar.CalendarSelectionStateChanged -= SingleCalendarSelectionStateChanged;
+ groupedAccountCalendar.CollectiveSelectionStateChanged -= SingleGroupCalendarCollectiveStateChanged;
+
+ _internalGroupedAccountCalendars.Remove(groupedAccountCalendar);
+ }
+
+ public void ClearGroupedAccountCalendar()
+ {
+ foreach (var groupedAccountCalendar in _internalGroupedAccountCalendars)
+ {
+ RemoveGroupedAccountCalendar(groupedAccountCalendar);
+ }
+ }
+
+ public void AddAccountCalendar(AccountCalendarViewModel accountCalendar)
+ {
+ // Find the group that this calendar belongs to.
+ var group = _internalGroupedAccountCalendars.FirstOrDefault(g => g.Account.Id == accountCalendar.Account.Id);
+
+ if (group == null)
+ {
+ // If the group doesn't exist, create it.
+ group = new GroupedAccountCalendarViewModel(accountCalendar.Account, new[] { accountCalendar });
+ AddGroupedAccountCalendar(group);
+ }
+ else
+ {
+ group.AccountCalendars.Add(accountCalendar);
+ }
+ }
+
+ public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar)
+ {
+ var group = _internalGroupedAccountCalendars.FirstOrDefault(g => g.Account.Id == accountCalendar.Account.Id);
+
+ // We don't expect but just in case.
+ if (group == null) return;
+
+ group.AccountCalendars.Remove(accountCalendar);
+
+ if (group.AccountCalendars.Count == 0)
+ {
+ RemoveGroupedAccountCalendar(group);
+ }
+ }
+}
diff --git a/Wino.Mail.WinUI/Services/NavigationService.cs b/Wino.Mail.WinUI/Services/NavigationService.cs
index 684740fc..76da25d8 100644
--- a/Wino.Mail.WinUI/Services/NavigationService.cs
+++ b/Wino.Mail.WinUI/Services/NavigationService.cs
@@ -3,18 +3,21 @@ using System.Linq;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
+using Wino.Calendar.Views;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation;
-using Wino.Mail.WinUI;
-using Wino.Mail.WinUI.Interfaces;
-using Wino.Mail.WinUI.Services;
using Wino.Helpers;
using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
+using Wino.Mail.WinUI;
+using Wino.Mail.WinUI.Interfaces;
+using Wino.Mail.WinUI.Services;
+using Wino.Mail.WinUI.Views.Calendar;
using Wino.Messaging.Client.Mails;
using Wino.Views;
using Wino.Views.Account;
+using Wino.Views.Mail;
using Wino.Views.Settings;
namespace Wino.Services;
@@ -62,6 +65,8 @@ public class NavigationService : NavigationServiceBase, INavigationService
WinoPage.KeyboardShortcutsPage => typeof(KeyboardShortcutsPage),
WinoPage.ContactsPage => typeof(ContactsPage),
WinoPage.SignatureAndEncryptionPage => typeof(SignatureAndEncryptionPage),
+ WinoPage.CalendarPage => typeof(CalendarPage),
+ WinoPage.EventDetailsPage => typeof(EventDetailsPage),
_ => null,
};
}
@@ -71,16 +76,36 @@ public class NavigationService : NavigationServiceBase, INavigationService
if (WinoApplication.MainWindow is not IWinoShellWindow shellWindow) throw new ArgumentException("MainWindow must implement IWinoShellWindow");
if (shellWindow.GetMainFrame() is not Frame mainFrame) throw new ArgumentException("MainFrame cannot be null.");
+ if (frameType == NavigationReferenceFrame.ShellFrame) return shellWindow.GetMainFrame();
+
return WinoVisualTreeHelper.GetChildObject(mainFrame.Content as UIElement, frameType.ToString());
}
+ public bool ChangeApplicationMode(WinoApplicationMode mode)
+ {
+ var coreFrame = GetCoreFrame(NavigationReferenceFrame.ShellFrame);
+
+ if (coreFrame == null) return false;
+
+ if (mode == WinoApplicationMode.Mail)
+ {
+ coreFrame.Navigate(typeof(MailAppShell), null);
+ }
+ else
+ {
+ coreFrame.Navigate(typeof(CalendarAppShell), null);
+ }
+
+ return true;
+ }
+
public bool Navigate(WinoPage page,
object? parameter = null,
- NavigationReferenceFrame frame = NavigationReferenceFrame.ShellFrame,
+ NavigationReferenceFrame frame = NavigationReferenceFrame.InnerShellFrame,
NavigationTransitionType transition = NavigationTransitionType.None)
{
var pageType = GetPageType(page);
- Frame shellFrame = GetCoreFrame(NavigationReferenceFrame.ShellFrame);
+ Frame shellFrame = GetCoreFrame(NavigationReferenceFrame.InnerShellFrame);
_statePersistanceService.IsReadingMail = _renderingPageTypes.Contains(page);
diff --git a/Wino.Mail.WinUI/ShellWindow.xaml b/Wino.Mail.WinUI/ShellWindow.xaml
index 4819459d..7bf5173a 100644
--- a/Wino.Mail.WinUI/ShellWindow.xaml
+++ b/Wino.Mail.WinUI/ShellWindow.xaml
@@ -3,6 +3,7 @@
x:Class="Wino.Mail.WinUI.ShellWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain"
xmlns:local="using:Wino.Mail.WinUI"
@@ -31,7 +32,30 @@
Background="Transparent"
IsBackButtonVisible="{x:Bind StatePersistanceService.IsBackButtonVisible, Mode=OneWay}"
IsPaneToggleButtonVisible="True"
- PaneToggleRequested="PaneButtonClicked" />
+ PaneToggleRequested="PaneButtonClicked">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
() ?? throw new Exception("StatePersistanceService not registered in DI container.");
public IPreferencesService PreferencesService { get; } = WinoApplication.Current.Services.GetService() ?? throw new Exception("PreferencesService not registered in DI container.");
+ public INavigationService NavigationService { get; } = WinoApplication.Current.Services.GetService() ?? throw new Exception("NavigationService not registered in DI container.");
public ICommand ShowWinoCommand { get; set; }
public ICommand ExitWinoCommand { get; set; }
@@ -70,7 +71,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient
{
// TODO: Handle protocol activations.
- MainShellFrame.Navigate(typeof(AppShell));
+ MainShellFrame.Navigate(typeof(MailAppShell));
}
public Microsoft.UI.Xaml.Controls.TitleBar GetTitleBar() => ShellTitleBar;
@@ -98,7 +99,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient
public void Receive(TitleBarShellContentUpdated message)
{
- if (MainShellFrame.Content is AppShell shellPage)
+ if (MainShellFrame.Content is MailAppShell shellPage)
{
ShellTitleBar.Content = shellPage.TopShellContent;
}
@@ -161,6 +162,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient
private void RestoreFromTray()
{
+
this.Show();
BringToFront();
}
@@ -193,4 +195,19 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient
WeakReferenceMessenger.Default.Unregister(this);
WeakReferenceMessenger.Default.Unregister(this);
}
+
+ private void SegmentedChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (sender is Segmented segmentedControl)
+ {
+ if (segmentedControl.SelectedIndex == 0)
+ {
+ NavigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail);
+ }
+ else
+ {
+ NavigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Calendar);
+ }
+ }
+ }
}
diff --git a/Wino.Mail.WinUI/Styles/CalendarThemeResources.xaml b/Wino.Mail.WinUI/Styles/CalendarThemeResources.xaml
new file mode 100644
index 00000000..c72bfed6
--- /dev/null
+++ b/Wino.Mail.WinUI/Styles/CalendarThemeResources.xaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+ #b2bec3
+ #dfe4ea
+ #2e86de
+
+
+ #2e86de
+ #dfe4ea
+
+
+ #000000
+ #000000
+ #000000
+
+
+
+
+
+ #525252
+ #262626
+ #121212
+
+
+ #3d3d3d
+ #4b4b4b
+
+
+ #000000
+ #FFFFFF
+ #000000
+
+
+
diff --git a/Wino.Mail.WinUI/Styles/DayHeaderControl.xaml b/Wino.Mail.WinUI/Styles/DayHeaderControl.xaml
new file mode 100644
index 00000000..396a0cf0
--- /dev/null
+++ b/Wino.Mail.WinUI/Styles/DayHeaderControl.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Styles/WinoCalendarResources.xaml b/Wino.Mail.WinUI/Styles/WinoCalendarResources.xaml
new file mode 100644
index 00000000..38ee859e
--- /dev/null
+++ b/Wino.Mail.WinUI/Styles/WinoCalendarResources.xaml
@@ -0,0 +1,424 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Styles/WinoCalendarResources.xaml.cs b/Wino.Mail.WinUI/Styles/WinoCalendarResources.xaml.cs
new file mode 100644
index 00000000..9fbc60f5
--- /dev/null
+++ b/Wino.Mail.WinUI/Styles/WinoCalendarResources.xaml.cs
@@ -0,0 +1,11 @@
+using Microsoft.UI.Xaml;
+
+namespace Wino.Styles;
+
+public sealed partial class WinoCalendarResources : ResourceDictionary
+{
+ public WinoCalendarResources()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/Wino.Mail.WinUI/Styles/WinoCalendarTypeSelectorControl.xaml b/Wino.Mail.WinUI/Styles/WinoCalendarTypeSelectorControl.xaml
new file mode 100644
index 00000000..48fb8cb6
--- /dev/null
+++ b/Wino.Mail.WinUI/Styles/WinoCalendarTypeSelectorControl.xaml
@@ -0,0 +1,83 @@
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Styles/WinoCalendarView.xaml b/Wino.Mail.WinUI/Styles/WinoCalendarView.xaml
new file mode 100644
index 00000000..8259f570
--- /dev/null
+++ b/Wino.Mail.WinUI/Styles/WinoCalendarView.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Styles/WinoDayTimelineCanvas.xaml b/Wino.Mail.WinUI/Styles/WinoDayTimelineCanvas.xaml
new file mode 100644
index 00000000..96257f2f
--- /dev/null
+++ b/Wino.Mail.WinUI/Styles/WinoDayTimelineCanvas.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Views/Abstract/CalendarAppShellAbstract .cs b/Wino.Mail.WinUI/Views/Abstract/CalendarAppShellAbstract .cs
new file mode 100644
index 00000000..9a92e942
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Abstract/CalendarAppShellAbstract .cs
@@ -0,0 +1,6 @@
+using Wino.Calendar.ViewModels;
+using Wino.Mail.WinUI;
+
+namespace Wino.Mail.Views.Abstract;
+
+public abstract class CalendarAppShellAbstract : BasePage { }
diff --git a/Wino.Mail.WinUI/Views/Abstract/CalendarPageAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/CalendarPageAbstract.cs
new file mode 100644
index 00000000..4a7ac8f8
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Abstract/CalendarPageAbstract.cs
@@ -0,0 +1,6 @@
+using Wino.Calendar.ViewModels;
+using Wino.Mail.WinUI;
+
+namespace Wino.Calendar.Views.Abstract;
+
+public abstract class CalendarPageAbstract : BasePage { }
diff --git a/Wino.Mail.WinUI/Views/Abstract/EventDetailsPageAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/EventDetailsPageAbstract.cs
new file mode 100644
index 00000000..620935a5
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Abstract/EventDetailsPageAbstract.cs
@@ -0,0 +1,5 @@
+using Wino.Calendar.ViewModels;
+
+namespace Wino.Mail.WinUI.Views.Abstract;
+
+public abstract class EventDetailsPageAbstract : BasePage { }
diff --git a/Wino.Mail.WinUI/Views/Abstract/AppShellAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/MailAppShellAbstract.cs
similarity index 53%
rename from Wino.Mail.WinUI/Views/Abstract/AppShellAbstract.cs
rename to Wino.Mail.WinUI/Views/Abstract/MailAppShellAbstract.cs
index 4a28d0d8..f48ceae9 100644
--- a/Wino.Mail.WinUI/Views/Abstract/AppShellAbstract.cs
+++ b/Wino.Mail.WinUI/Views/Abstract/MailAppShellAbstract.cs
@@ -1,8 +1,8 @@
-using Wino.Mail.WinUI;
using Wino.Mail.ViewModels;
+using Wino.Mail.WinUI;
namespace Wino.Views.Abstract;
-public abstract class AppShellAbstract : BasePage
+public abstract class MailAppShellAbstract : BasePage
{
}
diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml
new file mode 100644
index 00000000..919878c8
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml
@@ -0,0 +1,402 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml.cs b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml.cs
new file mode 100644
index 00000000..b50f0580
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml.cs
@@ -0,0 +1,66 @@
+using CommunityToolkit.Mvvm.Messaging;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Wino.Mail.Views.Abstract;
+using Wino.Messaging.Client.Calendar;
+
+namespace Wino.Mail.WinUI.Views.Calendar;
+
+public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
+ IRecipient
+{
+ private const string STATE_HorizontalCalendar = "HorizontalCalendar";
+ private const string STATE_VerticalCalendar = "VerticalCalendar";
+
+ public Frame GetShellFrame() => InnerShellFrame;
+
+ public CalendarAppShell()
+ {
+ InitializeComponent();
+
+ // Window.Current.SetTitleBar(DragArea);
+ ManageCalendarDisplayType();
+ }
+
+ private void ManageCalendarDisplayType()
+ {
+ // Go to different states based on the display type.
+ if (ViewModel.IsVerticalCalendar)
+ {
+ VisualStateManager.GoToState(this, STATE_VerticalCalendar, false);
+ }
+ else
+ {
+ VisualStateManager.GoToState(this, STATE_HorizontalCalendar, false);
+ }
+ }
+
+ private void PreviousDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoPreviousDateRequestedMessage());
+
+ private void NextDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoNextDateRequestedMessage());
+
+ public void Receive(CalendarDisplayTypeChangedMessage message)
+ {
+ ManageCalendarDisplayType();
+ }
+
+ //private void ShellFrameContentNavigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
+ // => RealAppBar.ShellFrameContent = (e.Content as BasePage).ShellContent;
+
+ //private void AppBarBackButtonClicked(Core.UWP.Controls.WinoAppTitleBar sender, RoutedEventArgs args)
+ // => ViewModel.NavigationService.GoBack();
+
+ protected override void RegisterRecipients()
+ {
+ base.RegisterRecipients();
+
+ WeakReferenceMessenger.Default.Register(this);
+ }
+
+ protected override void UnregisterRecipients()
+ {
+ base.UnregisterRecipients();
+
+ WeakReferenceMessenger.Default.Unregister(this);
+ }
+}
diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml
new file mode 100644
index 00000000..85d569cb
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml
@@ -0,0 +1,400 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml.cs b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml.cs
new file mode 100644
index 00000000..ee5e0500
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml.cs
@@ -0,0 +1,160 @@
+using System;
+using CommunityToolkit.Mvvm.Messaging;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Navigation;
+using Wino.Calendar.Args;
+using Wino.Calendar.Views.Abstract;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Calendar;
+using Wino.Messaging.Client.Calendar;
+
+namespace Wino.Calendar.Views;
+
+public sealed partial class CalendarPage : CalendarPageAbstract,
+ IRecipient,
+ IRecipient,
+ IRecipient,
+ IRecipient
+{
+ private const int PopupDialogOffset = 12;
+
+ public CalendarPage()
+ {
+ InitializeComponent();
+ NavigationCacheMode = NavigationCacheMode.Enabled;
+
+ ViewModel.DetailsShowCalendarItemChanged += CalendarItemDetailContextChanged;
+ }
+
+ private void CalendarItemDetailContextChanged(object sender, EventArgs e)
+ {
+ if (ViewModel.DisplayDetailsCalendarItemViewModel != null)
+ {
+ var control = CalendarControl.GetCalendarItemControl(ViewModel.DisplayDetailsCalendarItemViewModel);
+
+ if (control != null)
+ {
+ EventDetailsPopup.PlacementTarget = control;
+ }
+ }
+ }
+
+ 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();
+ public void Receive(GoPreviousDateRequestedMessage message) => CalendarControl.GoPreviousRange();
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ base.OnNavigatedTo(e);
+
+ if (e.NavigationMode == NavigationMode.Back) return;
+
+ if (e.Parameter is CalendarPageNavigationArgs args)
+ {
+ if (args.RequestDefaultNavigation)
+ {
+ // Go today.
+ WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(DateTime.Now.Date, CalendarInitInitiative.App));
+ }
+ else
+ {
+ // Go specified date.
+ WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(args.NavigationDate, CalendarInitInitiative.User));
+ }
+ }
+ }
+
+ private void CellSelected(object sender, TimelineCellSelectedArgs e)
+ {
+ // Dismiss event details if exists and cancel the selection.
+ // This is to prevent the event details from being displayed when the user clicks somewhere else.
+
+ if (EventDetailsPopup.IsOpen)
+ {
+ CalendarControl.UnselectActiveTimelineCell();
+ ViewModel.DisplayDetailsCalendarItemViewModel = null;
+
+ return;
+ }
+
+ ViewModel.SelectedQuickEventDate = e.ClickedDate;
+
+ TeachingTipPositionerGrid.Width = e.CellSize.Width;
+ TeachingTipPositionerGrid.Height = e.CellSize.Height;
+
+ Canvas.SetLeft(TeachingTipPositionerGrid, e.PositionerPoint.X);
+ Canvas.SetTop(TeachingTipPositionerGrid, e.PositionerPoint.Y);
+
+ // Adjust the start and end time in the flyout.
+ var startTime = ViewModel.SelectedQuickEventDate.Value.TimeOfDay;
+ var endTime = startTime.Add(TimeSpan.FromMinutes(30));
+
+ ViewModel.SelectQuickEventTimeRange(startTime, endTime);
+
+ QuickEventPopupDialog.IsOpen = true;
+ }
+
+ private void CellUnselected(object sender, TimelineCellUnselectedArgs e)
+ {
+ QuickEventPopupDialog.IsOpen = false;
+ }
+
+ private void QuickEventAccountSelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ QuickEventAccountSelectorFlyout.Hide();
+ }
+
+ private void QuickEventPopupClosed(object sender, object e)
+ {
+ // Reset the timeline selection when the tip is closed.
+ CalendarControl.ResetTimelineSelection();
+ }
+
+ private void PopupPlacementChanged(object sender, object e)
+ {
+ if (sender is Popup senderPopup)
+ {
+ // When the quick event Popup is positioned for different calendar types,
+ // we must adjust the offset to make sure the tip is not hidden and has nice
+ // spacing from the cell.
+
+ switch (senderPopup.ActualPlacement)
+ {
+ case PopupPlacementMode.Top:
+ senderPopup.VerticalOffset = PopupDialogOffset * -1;
+ break;
+ case PopupPlacementMode.Bottom:
+ senderPopup.VerticalOffset = PopupDialogOffset;
+ break;
+ case PopupPlacementMode.Left:
+ senderPopup.HorizontalOffset = PopupDialogOffset * -1;
+ break;
+ case PopupPlacementMode.Right:
+ senderPopup.HorizontalOffset = PopupDialogOffset;
+ break;
+ default:
+ break;
+ }
+ }
+
+ }
+
+ private void StartTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
+ => ViewModel.SelectedStartTimeString = args.Text;
+
+ private void EndTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
+ => ViewModel.SelectedEndTimeString = args.Text;
+
+ private void EventDetailsPopupClosed(object sender, object e)
+ {
+ ViewModel.DisplayDetailsCalendarItemViewModel = null;
+ }
+
+ private void CalendarScrolling(object sender, EventArgs e)
+ {
+ // In case of scrolling, we must dismiss the event details dialog.
+ ViewModel.DisplayDetailsCalendarItemViewModel = null;
+ }
+}
diff --git a/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml b/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml
new file mode 100644
index 00000000..7738d65f
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml
@@ -0,0 +1,296 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml.cs b/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml.cs
new file mode 100644
index 00000000..3c0d248d
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml.cs
@@ -0,0 +1,11 @@
+using Wino.Mail.WinUI.Views.Abstract;
+
+namespace Wino.Calendar.Views;
+
+public sealed partial class EventDetailsPage : EventDetailsPageAbstract
+{
+ public EventDetailsPage()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/Wino.Mail.WinUI/Views/ComposePage.xaml b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml
similarity index 93%
rename from Wino.Mail.WinUI/Views/ComposePage.xaml
rename to Wino.Mail.WinUI/Views/Mail/ComposePage.xaml
index 0f918878..29f277ed 100644
--- a/Wino.Mail.WinUI/Views/ComposePage.xaml
+++ b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml
@@ -1,5 +1,5 @@
-
+
-
-
+
+
+ SelectedItem="{x:Bind ViewModel.SelectedSigningCertificate, Mode=TwoWay}">
-
-
-
-
+
+
+
+
-
-
+
+
@@ -474,14 +477,17 @@
-
+
-
-
+
+
diff --git a/Wino.Mail.WinUI/Views/ComposePage.xaml.cs b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs
similarity index 99%
rename from Wino.Mail.WinUI/Views/ComposePage.xaml.cs
rename to Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs
index 0be2d50d..a3b99c69 100644
--- a/Wino.Mail.WinUI/Views/ComposePage.xaml.cs
+++ b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs
@@ -20,13 +20,13 @@ using Windows.UI.Core.Preview;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Models.Reader;
-using Wino.Mail.WinUI.Extensions;
using Wino.Mail.ViewModels.Data;
+using Wino.Mail.WinUI.Extensions;
using Wino.Messaging.Client.Mails;
using Wino.Messaging.Client.Shell;
using Wino.Views.Abstract;
-namespace Wino.Views;
+namespace Wino.Views.Mail;
public sealed partial class ComposePage : ComposePageAbstract,
IRecipient,
@@ -308,8 +308,8 @@ public sealed partial class ComposePage : ComposePageAbstract,
if (sender is Button senderButton && senderButton.Tag is MessageImportance importance)
{
ViewModel.SelectedMessageImportance = importance;
- if (ImportanceSplitButton.Content is Viewbox viewbox &&
- viewbox.Child is SymbolIcon symbolIcon &&
+ if (ImportanceSplitButton.Content is Viewbox viewbox &&
+ viewbox.Child is SymbolIcon symbolIcon &&
senderButton.Content is SymbolIcon contentIcon)
{
symbolIcon.Symbol = contentIcon.Symbol;
diff --git a/Wino.Mail.WinUI/Views/IdlePage.xaml b/Wino.Mail.WinUI/Views/Mail/IdlePage.xaml
similarity index 93%
rename from Wino.Mail.WinUI/Views/IdlePage.xaml
rename to Wino.Mail.WinUI/Views/Mail/IdlePage.xaml
index 877ca64a..d060ddc9 100644
--- a/Wino.Mail.WinUI/Views/IdlePage.xaml
+++ b/Wino.Mail.WinUI/Views/Mail/IdlePage.xaml
@@ -1,5 +1,5 @@
,
diff --git a/Wino.Mail.WinUI/Views/MailRenderingPage.xaml b/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml
similarity index 92%
rename from Wino.Mail.WinUI/Views/MailRenderingPage.xaml
rename to Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml
index 93808935..6b84a962 100644
--- a/Wino.Mail.WinUI/Views/MailRenderingPage.xaml
+++ b/Wino.Mail.WinUI/Views/Mail/MailRenderingPage.xaml
@@ -1,5 +1,5 @@
-
+
-
-
-
+ Visibility="{x:Bind ViewModel.IsSmimeSigned, Mode=OneWay}">
+
+
+
+ Style="{ThemeResource CautionIconInfoBadgeStyle}"
+ Visibility="{x:Bind ViewModel.SmimeSignaturesInvalid, Mode=OneWay}" />
+ Style="{ThemeResource SuccessIconInfoBadgeStyle}"
+ Visibility="{x:Bind ViewModel.SmimeSignaturesValid, Mode=OneWay}" />
-
-
-
-
+
+
+
+
,
diff --git a/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj b/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj
index cf6a63b7..dc8ed32a 100644
--- a/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj
+++ b/Wino.Mail.WinUI/Wino.Mail.WinUI.csproj
@@ -90,6 +90,15 @@
+
+
+
+
+
+
+
+
+
@@ -152,6 +161,7 @@
+
@@ -173,7 +183,7 @@
-
+
Designer
@@ -279,6 +289,40 @@
Designer
+
+
+
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+