Initial integration.

This commit is contained in:
Burak Kaan Köse
2025-12-26 20:46:48 +01:00
parent 10b85ea135
commit 014b5aa671
79 changed files with 4694 additions and 432 deletions
+26 -26
View File
@@ -7,13 +7,13 @@
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.Diagnostics" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250926-build.2293" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.DependencyPropertyGenerator" Version="0.1.250926-build.2293" />
@@ -23,22 +23,22 @@
<PackageVersion Include="Ical.Net" Version="4.3.1" />
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Graph" Version="5.96.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Graph" Version="5.99.0" />
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageVersion Include="Microsoft.Identity.Client" Version="4.79.1" />
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.79.1" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.79.1" />
<PackageVersion Include="Microsoft.Identity.Client" Version="4.79.2" />
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.79.2" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.79.2" />
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
<PackageVersion Include="MimeKit" Version="4.14.0" />
<PackageVersion Include="morelinq" Version="4.4.0" />
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
<PackageVersion Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
<PackageVersion Include="NodaTime" Version="3.2.2" />
<PackageVersion Include="Sentry.Serilog" Version="5.16.2" />
<PackageVersion Include="NodaTime" Version="3.2.3" />
<PackageVersion Include="Sentry.Serilog" Version="6.0.0" />
<PackageVersion Include="Serilog" Version="4.3.0" />
<PackageVersion Include="Serilog.Exceptions" Version="8.4.0" />
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
@@ -46,24 +46,24 @@
<PackageVersion Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
<PackageVersion Include="SkiaSharp" Version="3.119.1" />
<PackageVersion Include="sqlite-net-pcl" Version="1.10.196-beta" />
<PackageVersion Include="System.Drawing.Common" Version="10.0.0" />
<PackageVersion Include="System.Drawing.Common" Version="10.0.1" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
<PackageVersion Include="H.NotifyIcon.WinUI" Version="2.3.2" />
<PackageVersion Include="H.NotifyIcon.WinUI" Version="2.4.1" />
<PackageVersion Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageVersion Include="Google.Apis.Auth" Version="1.72.0" />
<PackageVersion Include="Google.Apis.Calendar.v3" Version="1.72.0.3953" />
<PackageVersion Include="Google.Apis.Gmail.v1" Version="1.70.0.3833" />
<PackageVersion Include="Google.Apis.PeopleService.v1" Version="1.69.0.3785" />
<PackageVersion Include="Google.Apis.Auth" Version="1.73.0" />
<PackageVersion Include="Google.Apis.Calendar.v3" Version="1.73.0.3993" />
<PackageVersion Include="Google.Apis.Gmail.v1" Version="1.73.0.3987" />
<PackageVersion Include="Google.Apis.PeopleService.v1" Version="1.72.0.3973" />
<PackageVersion Include="HtmlKit" Version="1.2.0" />
<PackageVersion Include="MailKit" Version="4.14.1" />
<PackageVersion Include="TimePeriodLibrary.NET" Version="2.1.6" />
<PackageVersion Include="System.Reactive" Version="6.1.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.1" />
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.1" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.250930001-experimental1" />
<PackageVersion Include="WinUIEx" Version="2.9.0" />
</ItemGroup>
@@ -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);
}
}
@@ -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<TokenInformationEx, AuthorizationRequested>(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<MailSynchronizationResult, NewMailSynchronizationRequested>(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<CalendarSynchronizationResult, NewCalendarSynchronizationRequested>(new NewCalendarSynchronizationRequested(synchronizationOptions, SynchronizationSource.Client));
}
}
@@ -23,7 +23,7 @@ using Wino.Messaging.Server;
namespace Wino.Calendar.ViewModels;
public partial class AppShellViewModel : CalendarBaseViewModel,
public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
IRecipient<VisibleDateRangeChangedMessage>,
IRecipient<CalendarEnableStatusChangedMessage>,
IRecipient<NavigateManageAccountsRequested>,
@@ -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);
}
@@ -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);
@@ -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();
}
}
@@ -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;
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Platforms>x86;x64;arm64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
@@ -30,6 +30,18 @@ public class CalendarItem : ICalendarItem
public TimeSpan StartDateOffset { get; set; }
public TimeSpan EndDateOffset { get; set; }
/// <summary>
/// IANA timezone identifier for the start time (e.g., "America/New_York", "Europe/London").
/// If null or empty, UTC is assumed.
/// </summary>
public string StartTimeZone { get; set; }
/// <summary>
/// IANA timezone identifier for the end time (e.g., "America/New_York", "Europe/London").
/// If null or empty, UTC is assumed.
/// </summary>
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
};
}
/// <summary>
/// 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.
/// </summary>
[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);
}
}
/// <summary>
/// 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.
/// </summary>
[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);
}
}
}
@@ -3,5 +3,6 @@
public enum NavigationReferenceFrame
{
ShellFrame,
InnerShellFrame,
RenderingFrame
}
@@ -0,0 +1,7 @@
namespace Wino.Core.Domain.Enums;
public enum WinoApplicationMode
{
Mail,
Calendar
}
@@ -64,6 +64,12 @@ public interface ISynchronizationManager
Task<MailSynchronizationResult> SynchronizeProfileAsync(Guid accountId,
CancellationToken cancellationToken = default);
/// <summary>
/// Handles calendar synchronization for the given account.
/// </summary>
Task<CalendarSynchronizationResult> SynchronizeCalendarAsync(CalendarSynchronizationOptions options,
CancellationToken cancellationToken = default);
/// <summary>
/// Downloads a MIME message for the given mail item.
/// </summary>
@@ -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);
}
@@ -177,6 +177,15 @@ public static class GoogleIntegratorExtensions
return null;
}
/// <summary>
/// Extracts the timezone string from EventDateTime.
/// Returns null for all-day events or if timezone is not specified.
/// </summary>
public static string GetEventTimeZone(EventDateTime eventDateTime)
{
return eventDateTime?.TimeZone;
}
/// <summary>
/// RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545.
/// </summary>
@@ -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)
{
@@ -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,
@@ -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)
@@ -299,6 +299,56 @@ public class SynchronizationManager : ISynchronizationManager
return await SynchronizeMailAsync(options, cancellationToken);
}
/// <summary>
/// Handles calendar synchronization for the given account.
/// </summary>
/// <param name="options">Calendar synchronization options</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Synchronization result</returns>
public async Task<CalendarSynchronizationResult> 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;
}
}
/// <summary>
/// Downloads a MIME message for the given mail item.
/// </summary>
+6 -18
View File
@@ -1631,8 +1631,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
{
_logger.Information("No calendar sync identifier for calendar {Name}. Performing initial sync.", calendar.Name);
var startDate = DateTime.UtcNow.AddYears(-2).ToString("u");
var endDate = DateTime.UtcNow.ToString("u");
var startDate = DateTimeOffset.UtcNow.AddYears(-2).ToString("o");
var endDate = DateTimeOffset.UtcNow.ToString("o");
eventsDeltaResponse = await _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.GetAsDeltaGetResponseAsync((requestConfiguration) =>
{
@@ -1658,20 +1658,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
_logger.Information("Performing delta sync for calendar {Name}.", calendar.Name);
var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.ToGetRequestInformation((requestConfiguration) =>
{
//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<RequestInformation, Message,
var deltaToken = GetDeltaTokenFromDeltaLink(latestDeltaLink);
await _outlookChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, deltaToken).ConfigureAwait(false);
/// await _outlookChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, deltaToken).ConfigureAwait(false);
}
}
return default;
// TODO: Return proper results.
return CalendarSynchronizationResult.Empty;
}
private async Task SynchronizeCalendarsAsync(CancellationToken cancellationToken = default)
@@ -27,7 +27,7 @@ using Wino.Messaging.UI;
namespace Wino.Mail.ViewModels;
public partial class AppShellViewModel : MailBaseViewModel,
public partial class MailAppShellViewModel : MailBaseViewModel,
IRecipient<NavigateManageAccountsRequested>,
IRecipient<MailtoProtocolMessageRequested>,
IRecipient<RefreshUnreadCountsMessage>,
@@ -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<AccountCreatedMessage>(this);
Messenger.Unregister<AccountRemovedMessage>(this);
Messenger.Unregister<AccountUpdatedMessage>(this);
Messenger.Unregister<NavigateManageAccountsRequested>(this);
Messenger.Unregister<MailtoProtocolMessageRequested>(this);
Messenger.Unregister<RefreshUnreadCountsMessage>(this);
+4 -2
View File
@@ -43,7 +43,9 @@ public class MailBaseViewModel : CoreBaseViewModel,
protected override void RegisterRecipients()
{
base.RegisterRecipients();
UnregisterRecipients();
Messenger.Register<MailAddedMessage>(this);
Messenger.Register<MailRemovedMessage>(this);
Messenger.Register<MailUpdatedMessage>(this);
@@ -58,7 +60,7 @@ public class MailBaseViewModel : CoreBaseViewModel,
protected override void UnregisterRecipients()
{
base.UnregisterRecipients();
Messenger.Unregister<MailAddedMessage>(this);
Messenger.Unregister<MailRemovedMessage>(this);
Messenger.Unregister<MailUpdatedMessage>(this);
@@ -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();
}
}
+7
View File
@@ -18,7 +18,14 @@
<ResourceDictionary Source="Styles/WebViewEditorControl.xaml" />
<ResourceDictionary Source="Controls/ListView/WinoListViewStyles.xaml" />
<ResourceDictionary Source="Styles/CalendarThemeResources.xaml" />
<ResourceDictionary Source="Styles/WinoDayTimelineCanvas.xaml" />
<ResourceDictionary Source="Styles/WinoCalendarView.xaml" />
<ResourceDictionary Source="Styles/WinoCalendarTypeSelectorControl.xaml" />
<styles:WinoExpanderStyle />
<styles:WinoCalendarResources />
<ResourceDictionary Source="AppThemes/Default.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
+19 -7
View File
@@ -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<NewMailSynchronizationRequested>
public partial class App : WinoApplication,
IRecipient<NewMailSynchronizationRequested>,
IRecipient<NewCalendarSynchronizationRequested>
{
private ISynchronizationManager? _synchronizationManager;
@@ -60,11 +66,13 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
services.AddTransient<ISettingsBuilderService, SettingsBuilderService>();
services.AddTransient<IProviderService, ProviderService>();
services.AddSingleton<IAuthenticatorConfig, MailAuthenticatorConfiguration>();
services.AddSingleton<IAccountCalendarStateService, AccountCalendarStateService>();
}
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<NewMailSynchronizationReq
services.AddTransient(typeof(AliasManagementPageViewModel));
services.AddTransient(typeof(ContactsPageViewModel));
services.AddTransient(typeof(SignatureAndEncryptionPageViewModel));
services.AddTransient(typeof(CalendarPageViewModel));
services.AddTransient(typeof(CalendarSettingsPageViewModel));
services.AddTransient(typeof(EventDetailsPageViewModel));
}
#endregion
@@ -93,7 +105,7 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
{
var services = new ServiceCollection();
services.RegisterViewModelService();
services.RegisterCoreServices();
services.RegisterSharedServices();
services.RegisterCoreUWPServices();
services.RegisterCoreViewModels();
@@ -347,12 +359,11 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
private void RegisterRecipients()
{
WeakReferenceMessenger.Default.Register<NewMailSynchronizationRequested>(this);
WeakReferenceMessenger.Default.Register<NewCalendarSynchronizationRequested>(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);
/// <summary>
/// Handles activation redirected from another instance (single-instancing).
@@ -378,4 +389,5 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
}
});
}
}
@@ -0,0 +1,30 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.Controls;
public partial class CalendarItemCommandBarFlyout : CommandBarFlyout
{
public static readonly DependencyProperty ItemProperty = DependencyProperty.Register(nameof(Item), typeof(CalendarItemViewModel), typeof(CalendarItemCommandBarFlyout), new PropertyMetadata(null, new PropertyChangedCallback(OnItemChanged)));
public CalendarItemViewModel Item
{
get { return (CalendarItemViewModel)GetValue(ItemProperty); }
set { SetValue(ItemProperty, value); }
}
private static void OnItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CalendarItemCommandBarFlyout flyout)
{
flyout.UpdateMenuItems();
}
}
private void UpdateMenuItems()
{
}
}
@@ -0,0 +1,160 @@
<UserControl
x:Class="Wino.Calendar.Controls.CalendarItemControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Mail.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Wino.Helpers"
xmlns:local="using:Wino.Calendar.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
d:DesignHeight="300"
d:DesignWidth="400"
CanDrag="True"
DragStarting="ControlDragStarting"
DropCompleted="ControlDropped"
mc:Ignorable="d">
<Grid
x:Name="MainGrid"
CornerRadius="4"
DoubleTapped="ControlDoubleTapped"
RightTapped="ControlRightTapped"
Tapped="ControlTapped"
ToolTipService.ToolTip="{x:Bind CalendarItemTitle, Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.ContextFlyout>
<local:CalendarItemCommandBarFlyout Placement="Top">
<local:CalendarItemCommandBarFlyout.PrimaryCommands>
<AppBarButton Icon="Save" Label="save" />
<AppBarButton Icon="Delete" Label="Delet" />
</local:CalendarItemCommandBarFlyout.PrimaryCommands>
</local:CalendarItemCommandBarFlyout>
</Grid.ContextFlyout>
<Grid
x:Name="MainBackground"
Grid.ColumnSpan="2"
Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}" />
<Rectangle
x:Name="MainBorder"
Grid.ColumnSpan="2"
Canvas.ZIndex="2"
Stroke="{ThemeResource CalendarItemBorderBrush}"
StrokeThickness="0" />
<TextBlock
x:Name="EventTitleTextblock"
Margin="2,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
CharacterSpacing="8"
FontSize="13"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
HorizontalTextAlignment="Center"
Text="{x:Bind CalendarItemTitle, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
<!-- TODO: Event attributes -->
<StackPanel
x:Name="AttributeStack"
Grid.Column="1"
Margin="0,4,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Orientation="Horizontal"
Spacing="6">
<controls:WinoFontIcon
FontSize="12"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
Icon="CalendarEventRepeat"
Visibility="{x:Bind CalendarItem.IsRecurringEvent, Mode=OneWay}" />
<controls:WinoFontIcon
FontSize="12"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
Icon="CalendarEventMuiltiDay"
Visibility="{x:Bind CalendarItem.IsMultiDayEvent, Mode=OneWay}" />
</StackPanel>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="SelectionStates">
<VisualState x:Name="NonSelected" />
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Target="MainBorder.StrokeThickness" Value="1" />
<Setter Target="MainBorder.Margin" Value="1" />
<Setter Target="MainBorder.Stroke" Value="{ThemeResource CalendarItemSelectedBorderBrush}" />
</VisualState.Setters>
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind CalendarItem.IsSelected, Mode=OneWay, FallbackValue=False}" />
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DraggingStates">
<VisualState x:Name="NotDragging" />
<VisualState x:Name="Dragging">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind IsDragging, Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="MainBorder.StrokeThickness" Value="2" />
<Setter Target="MainBorder.Stroke" Value="{ThemeResource CalendarItemDraggingBorderBrush}" />
<Setter Target="MainBorder.StrokeDashArray" Value="2.5" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="EventDurationStates">
<!-- Regular event template in the panel. -->
<VisualState x:Name="RegularEvent" />
<!-- All-Day template for top area. -->
<VisualState x:Name="AllDayEvent">
<VisualState.Setters>
<Setter Target="AttributeStack.VerticalAlignment" Value="Center" />
<Setter Target="MainGrid.MinHeight" Value="30" />
<Setter Target="MainBorder.StrokeThickness" Value="0" />
</VisualState.Setters>
</VisualState>
<!-- Multi-Day template for top area. -->
<VisualState x:Name="CustomAreaMultiDayEvent">
<VisualState.Setters>
<Setter Target="MainBackground.Opacity" Value="1" />
<Setter Target="MainBorder.StrokeThickness" Value="0" />
<Setter Target="AttributeStack.Visibility" Value="Visible" />
<Setter Target="AttributeStack.Margin" Value="0,0,4,0" />
<Setter Target="AttributeStack.VerticalAlignment" Value="Center" />
<Setter Target="MainGrid.MinHeight" Value="30" />
<Setter Target="EventTitleTextblock.HorizontalAlignment" Value="Stretch" />
<Setter Target="EventTitleTextblock.HorizontalTextAlignment" Value="Left" />
<Setter Target="EventTitleTextblock.Margin" Value="6,0" />
</VisualState.Setters>
</VisualState>
<!--
Ghost rendering for multi-day events in the panel.
All-Multi day area template is CustomAreaMultiDayEvent
-->
<VisualState x:Name="MultiDayEvent">
<VisualState.Setters>
<Setter Target="MainGrid.CornerRadius" Value="0" />
<Setter Target="MainBackground.Opacity" Value="0.2" />
<Setter Target="MainGrid.IsHitTestVisible" Value="False" />
<Setter Target="MainBorder.StrokeThickness" Value="0" />
<Setter Target="AttributeStack.Visibility" Value="Collapsed" />
<Setter Target="EventTitleTextblock.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>
@@ -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)));
/// <summary>
/// Whether the control is displaying as regular event or all-multi day area in the day control.
/// </summary>
public bool IsCustomEventArea
{
get { return (bool)GetValue(IsCustomEventAreaProperty); }
set { SetValue(IsCustomEventAreaProperty, value); }
}
/// <summary>
/// Day that the calendar item is rendered at.
/// It's needed for title manipulation and some other adjustments later on.
/// </summary>
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));
}
}
@@ -0,0 +1,42 @@
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
namespace Wino.Calendar.Controls;
/// <summary>
/// FlipView that hides the navigation buttons and exposes methods to navigate to the next and previous items with animations.
/// </summary>
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();
}
}
@@ -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();
}
}
@@ -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");
}
}
}
@@ -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<TimelineCellSelectedArgs> TimelineCellSelected;
public event EventHandler<TimelineCellUnselectedArgs> TimelineCellUnselected;
public event EventHandler ScrollPositionChanging;
#region Dependency Properties
public static readonly DependencyProperty DayRangesProperty = DependencyProperty.Register(nameof(DayRanges), typeof(ObservableCollection<DayRangeRenderModel>), 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));
/// <summary>
/// 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.
/// </summary>
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); }
}
/// <summary>
/// Gets or sets the collection of day ranges to render.
/// Each day range usually represents a week, but it may support other ranges.
/// </summary>
public ObservableCollection<DayRangeRenderModel> DayRanges
{
get { return (ObservableCollection<DayRangeRenderModel>)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<CalendarItemControl>().FirstOrDefault(a => a.CalendarItem == calendarItemViewModel);
}
}
@@ -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));
/// <summary>
/// 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.
/// </summary>
public WinoDayTimelineCanvas ActiveCanvas
{
get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); }
set { SetValue(ActiveCanvasProperty, value); }
}
/// <summary>
/// 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.
/// </summary>
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<FlipViewItem> 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<ScrollViewer>();
});
}
});
}
}
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<WinoDayTimelineCanvas>();
});
}
});
}
}
/// <summary>
/// Navigates to the specified date in the calendar.
/// </summary>
/// <param name="dateTime">Date to navigate.</param>
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<DayRangeRenderModel> GetItemsSource()
=> ItemsSource as ObservableRangeCollection<DayRangeRenderModel>;
}
@@ -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<ICalendarItem, CalendarItemMeasurement> _measurements = new Dictionary<ICalendarItem, CalendarItemMeasurement>();
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<ContentPresenter>();
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<ICalendarItem> events)
{
var columns = new List<List<ICalendarItem>>();
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<ICalendarItem> { 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<List<ICalendarItem>> 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<List<ICalendarItem>> 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
}
@@ -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;
}
}
@@ -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); }
}
/// <summary>
/// Gets or sets the command to execute when a date is picked.
/// Unused.
/// </summary>
public ICommand DateClickedCommand
{
get { return (ICommand)GetValue(DateClickedCommandProperty); }
set { SetValue(DateClickedCommandProperty, value); }
}
/// <summary>
/// Gets or sets the highlighted range of dates.
/// </summary>
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<CalendarViewDayItem>(CalendarView);
foreach (var calendarDayItem in markDateCalendarDayItems)
{
var border = WinoVisualTreeHelper.GetChildObject<Border>(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;
}
}
}
}
@@ -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<TimelineCellSelectedArgs> TimelineCellSelected;
public event EventHandler<TimelineCellUnselectedArgs> 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;
}
}
@@ -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();
/// <summary>
/// Returns full date + duration info in Event Details page details title.
/// </summary>
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);
}
}
@@ -1,5 +1,5 @@
<abstract:AppShellAbstract
x:Class="Wino.Views.AppShell"
<abstract:MailAppShellAbstract
x:Class="Wino.Views.MailAppShell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:abstract="using:Wino.Views.Abstract"
@@ -444,7 +444,7 @@
<!-- Main Content -->
<Frame
x:Name="ShellFrame"
x:Name="InnerShellFrame"
Padding="0,0,7,7"
IsNavigationStackEnabled="False"
Navigated="ShellFrameContentNavigated">
@@ -475,4 +475,4 @@
</muxc:NavigationView>
</Grid>
</abstract:AppShellAbstract>
</abstract:MailAppShellAbstract>
@@ -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<AccountMenuItemExtended>,
IRecipient<NavigateMailFolderEvent>,
IRecipient<CreateNewMailWithMultipleAccountsRequested>,
@@ -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<InfoBarMessageRequested>(this);
// WeakReferenceMessenger.Default.Register<AccountMenuItemExtended>(this);
// WeakReferenceMessenger.Default.Register<CreateNewMailWithMultipleAccountsRequested>(this);
// WeakReferenceMessenger.Default.Register<NavigateMailFolderEvent>(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);
}
}
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="Wino.Mail.WinUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Wino.Mail.WinUI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="Wino.Mail.WinUI"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid />
</Window>
-29
View File
@@ -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;
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
@@ -0,0 +1,40 @@
using System;
using Windows.Foundation;
namespace Wino.Calendar.Args;
/// <summary>
/// When a new timeline cell is selected.
/// </summary>
public class TimelineCellSelectedArgs : EventArgs
{
public TimelineCellSelectedArgs(DateTime clickedDate, Point canvasPoint, Point positionerPoint, Size cellSize)
{
ClickedDate = clickedDate;
CanvasPoint = canvasPoint;
PositionerPoint = positionerPoint;
CellSize = cellSize;
}
/// <summary>
/// Clicked date and time information for the cell.
/// </summary>
public DateTime ClickedDate { get; set; }
/// <summary>
/// Position relative to the cell drawing part of the canvas.
/// Used to detect clicked cell from the position.
/// </summary>
public Point CanvasPoint { get; }
/// <summary>
/// Position relative to the main root positioner element of the drawing canvas.
/// Used to show the create event dialog teaching tip in correct position.
/// </summary>
public Point PositionerPoint { get; }
/// <summary>
/// Size of the cell.
/// </summary>
public Size CellSize { get; }
}
@@ -0,0 +1,8 @@
using System;
namespace Wino.Calendar.Args;
/// <summary>
/// When selected timeline cell is unselected.
/// </summary>
public class TimelineCellUnselectedArgs : EventArgs { }
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
/// <summary>
/// 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.
/// </summary>
public partial class AccountCalendarStateService : ObservableObject, IAccountCalendarStateService
{
public event EventHandler<GroupedAccountCalendarViewModel> CollectiveAccountGroupSelectionStateChanged;
public event EventHandler<AccountCalendarViewModel> AccountCalendarSelectionStateChanged;
[ObservableProperty]
public partial ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> GroupedAccountCalendars { get; set; }
private ObservableCollection<GroupedAccountCalendarViewModel> _internalGroupedAccountCalendars = new ObservableCollection<GroupedAccountCalendarViewModel>();
public IEnumerable<AccountCalendarViewModel> ActiveCalendars
{
get
{
return GroupedAccountCalendars
.SelectMany(a => a.AccountCalendars)
.Where(b => b.IsChecked);
}
}
public IEnumerable<IGrouping<MailAccount, AccountCalendarViewModel>> GroupedAccountCalendarsEnumerable
{
get
{
return GroupedAccountCalendars
.Select(a => a.AccountCalendars)
.SelectMany(b => b)
.GroupBy(c => c.Account);
}
}
public AccountCalendarStateService()
{
GroupedAccountCalendars = new ReadOnlyObservableCollection<GroupedAccountCalendarViewModel>(_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);
}
}
}
+30 -5
View File
@@ -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<Frame>(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);
File diff suppressed because one or more lines are too long
+20 -3
View File
@@ -2,12 +2,12 @@ using System;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.Controls;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.UI;
using Wino.Core.Domain.Interfaces;
using Wino.Mail.WinUI;
using Wino.Mail.WinUI.Interfaces;
using Wino.Messaging.Client.Mails;
using Wino.Messaging.Client.Shell;
@@ -21,6 +21,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, IRecipient
{
public IStatePersistanceService StatePersistanceService { get; } = WinoApplication.Current.Services.GetService<IStatePersistanceService>() ?? throw new Exception("StatePersistanceService not registered in DI container.");
public IPreferencesService PreferencesService { get; } = WinoApplication.Current.Services.GetService<IPreferencesService>() ?? throw new Exception("PreferencesService not registered in DI container.");
public INavigationService NavigationService { get; } = WinoApplication.Current.Services.GetService<INavigationService>() ?? 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<TitleBarShellContentUpdated>(this);
WeakReferenceMessenger.Default.Unregister<ApplicationThemeChanged>(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);
}
}
}
}
@@ -0,0 +1,39 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StaticResource x:Key="WinoCalendarViewTodayBackgroundBrush" ResourceKey="SystemControlBackgroundAccentBrush" />
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<!-- CalendarControl -->
<SolidColorBrush x:Key="CalendarSeperatorBrush">#b2bec3</SolidColorBrush>
<SolidColorBrush x:Key="CalendarFieldWorkingHoursBackgroundBrush">#dfe4ea</SolidColorBrush>
<SolidColorBrush x:Key="CalendarFieldSelectedBackgroundBrush">#2e86de</SolidColorBrush>
<!-- CalendarView -->
<SolidColorBrush x:Key="WinoCalendarViewBorderBrush">#2e86de</SolidColorBrush>
<SolidColorBrush x:Key="WinoCalendarViewVisibleDayBackgroundBrush">#dfe4ea</SolidColorBrush>
<!-- Calendar Item Control -->
<SolidColorBrush x:Key="CalendarItemBorderBrush">#000000</SolidColorBrush>
<SolidColorBrush x:Key="CalendarItemSelectedBorderBrush">#000000</SolidColorBrush>
<SolidColorBrush x:Key="CalendarItemDraggingBorderBrush">#000000</SolidColorBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<!-- CalendarControl -->
<SolidColorBrush x:Key="CalendarSeperatorBrush">#525252</SolidColorBrush>
<SolidColorBrush x:Key="CalendarFieldWorkingHoursBackgroundBrush">#262626</SolidColorBrush>
<SolidColorBrush x:Key="CalendarFieldSelectedBackgroundBrush">#121212</SolidColorBrush>
<!-- CalendarView -->
<SolidColorBrush x:Key="WinoCalendarViewBorderBrush">#3d3d3d</SolidColorBrush>
<SolidColorBrush x:Key="WinoCalendarViewVisibleDayBackgroundBrush">#4b4b4b</SolidColorBrush>
<!-- Calendar Item Control -->
<SolidColorBrush x:Key="CalendarItemBorderBrush">#000000</SolidColorBrush>
<SolidColorBrush x:Key="CalendarItemSelectedBorderBrush">#FFFFFF</SolidColorBrush>
<SolidColorBrush x:Key="CalendarItemDraggingBorderBrush">#000000</SolidColorBrush>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
@@ -0,0 +1,23 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Calendar.Controls">
<!-- Left day header DayHeaderControl -->
<Style TargetType="controls:DayHeaderControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:DayHeaderControl">
<Grid>
<TextBlock
x:Name="PART_DayHeaderTextBlock"
HorizontalAlignment="Center"
VerticalAlignment="Top"
FontSize="12" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -0,0 +1,424 @@
<ResourceDictionary
x:Class="Wino.Styles.WinoCalendarResources"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Calendar.Controls"
xmlns:controls2="using:Wino.Mail.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:data="using:Wino.Calendar.ViewModels.Data"
xmlns:helpers="using:Wino.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:Wino.Core.Domain.Models.Calendar"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
xmlns:selectors="using:Wino.Calendar.Selectors"
xmlns:selectors1="using:Wino.Selectors"
xmlns:toolkitControls="using:CommunityToolkit.WinUI.Controls">
<!-- 08:00 or 8 AM/PM on the left etc. -->
<DataTemplate x:Key="DayCalendarHourHeaderTemplate" x:DataType="models:DayHeaderRenderModel">
<Grid Height="{x:Bind HourHeight}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Top"
Text="{x:Bind DayHeader}" />
</Grid>
</DataTemplate>
<!-- Vertical panel that renders items on canvas. -->
<DataTemplate x:Key="DayCalendarItemVerticalRenderTemplate" x:DataType="models:CalendarDayModel">
<ItemsControl x:Name="RegularEventItemsControl" ItemsSource="{x:Bind EventsCollection.RegularEvents}">
<ItemsControl.ItemTemplate>
<!-- Default Calendar Item View Model Template -->
<DataTemplate x:DataType="data:CalendarItemViewModel">
<controls:CalendarItemControl
CalendarItem="{x:Bind}"
DisplayingDate="{Binding ElementName=RegularEventItemsControl, Path=DataContext}"
IsCustomEventArea="False" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerTransitions>
<TransitionCollection>
<PaneThemeTransition Edge="Left" />
</TransitionCollection>
</ItemsControl.ItemContainerTransitions>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WinoCalendarPanel HourHeight="{Binding Path=CalendarRenderOptions.CalendarSettings.HourHeight}" Period="{Binding Path=Period}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
<!-- Equally distributed days of week representation in FlipView. -->
<!-- Used for day-week-work week templates. -->
<!-- Horizontal template -->
<DataTemplate x:Key="FlipTemplate" x:DataType="models:DayRangeRenderModel">
<Grid
x:Name="RootGrid"
Background="Transparent"
ColumnSpacing="0"
RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MinHeight="100" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ItemsControl Margin="50,0,16,0" ItemsSource="{x:Bind CalendarDays}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:CalendarDayModel">
<controls:DayColumnControl DayModel="{x:Bind}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<toolkitControls:UniformGrid
Columns="{Binding CalendarRenderOptions.TotalDayCount}"
Orientation="Horizontal"
Rows="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<ScrollViewer
Grid.Row="1"
Grid.ColumnSpan="2"
Margin="0"
Padding="0,0,16,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Rendering left hour headers. -->
<ItemsControl ItemTemplate="{StaticResource DayCalendarHourHeaderTemplate}" ItemsSource="{x:Bind DayHeaders}" />
<!-- Drawing canvas for timeline. -->
<controls:WinoDayTimelineCanvas
Grid.Column="1"
HalfHourSeperatorColor="{ThemeResource CalendarSeperatorBrush}"
PositionerUIElement="{Binding ElementName=RootGrid}"
RenderOptions="{x:Bind CalendarRenderOptions}"
SelectedCellBackgroundBrush="{ThemeResource CalendarFieldSelectedBackgroundBrush}"
SeperatorColor="{ThemeResource CalendarSeperatorBrush}"
WorkingHourCellBackgroundColor="{ThemeResource CalendarFieldWorkingHoursBackgroundBrush}" />
<!-- Each vertical day grids that renders events. -->
<ItemsControl
Grid.Column="1"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
ItemTemplate="{StaticResource DayCalendarItemVerticalRenderTemplate}"
ItemsSource="{x:Bind CalendarDays}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<toolkitControls:UniformGrid
Columns="{Binding CalendarRenderOptions.TotalDayCount}"
Orientation="Horizontal"
Rows="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</ScrollViewer>
</Grid>
</DataTemplate>
<!-- Template that displays 35 days in total. -->
<!-- Used for monthly view -->
<!-- Vertical template -->
<DataTemplate x:Key="MonthlyFlipTemplate" x:DataType="models:DayRangeRenderModel">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Height="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind CalendarDays}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:CalendarDayModel">
<controls:DayColumnControl DayModel="{x:Bind Mode=OneWay}" Template="{StaticResource MonthlyColumnControlTemplate}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls2:EqualGridPanel Columns="7" Rows="5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</DataTemplate>
<ItemsPanelTemplate x:Key="VerticalFlipViewItemsPanel">
<VirtualizingStackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="HorizontalFlipViewItemsPanel">
<VirtualizingStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
<!-- Default style for WinoCalendarControl -->
<Style TargetType="controls:WinoCalendarControl">
<Style.Setters>
<Setter Property="HorizontalItemsPanelTemplate" Value="{StaticResource HorizontalFlipViewItemsPanel}" />
<Setter Property="VerticalItemsPanelTemplate" Value="{StaticResource VerticalFlipViewItemsPanel}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:WinoCalendarControl">
<Grid>
<controls:WinoCalendarFlipView
x:Name="PART_WinoFlipView"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
ActiveCanvas="{x:Bind ActiveCanvas, Mode=TwoWay}"
ActiveVerticalScrollViewer="{x:Bind ActiveScrollViewer, Mode=TwoWay}"
Background="Transparent"
IsIdle="{x:Bind IsFlipIdle, Mode=TwoWay}"
IsTabStop="False"
ItemsSource="{TemplateBinding DayRanges}"
SelectedIndex="{Binding SelectedFlipViewIndex, RelativeSource={RelativeSource Mode=TemplatedParent}, Mode=TwoWay}">
<controls:WinoCalendarFlipView.ItemTemplateSelector>
<selectors1:WinoCalendarItemTemplateSelector
DayWeekWorkWeekTemplate="{StaticResource FlipTemplate}"
DisplayType="{x:Bind DisplayType, Mode=OneWay}"
MonthlyTemplate="{StaticResource MonthlyFlipTemplate}" />
</controls:WinoCalendarFlipView.ItemTemplateSelector>
</controls:WinoCalendarFlipView>
<Grid x:Name="PART_IdleGrid" Visibility="Collapsed">
<muxc:ProgressRing
Width="50"
Height="50"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsActive="True" />
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
<!-- Top header control for days. -->
<ControlTemplate x:Key="DailyColumnControlTemplate" TargetType="controls:DayColumnControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="7" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Name of the day. Monday, Tuesday etc. at the top. -->
<TextBlock
x:Name="PART_ColumnHeaderText"
Margin="8,0,0,0"
FontSize="16"
TextTrimming="CharacterEllipsis" />
<Grid
Grid.Row="2"
Grid.RowSpan="2"
BorderBrush="{ThemeResource CalendarSeperatorBrush}"
BorderThickness="1,1,0,1" />
<!-- Border for today indication. -->
<Border
x:Name="PART_IsTodayBorder"
Grid.Row="1"
Height="5"
Margin="2,0,2,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Background="{ThemeResource SystemAccentColor}"
CornerRadius="2"
Visibility="Collapsed" />
<!-- Place where full day events go. -->
<Grid
x:Name="PART_DayDataAreaGrid"
Grid.Row="2"
Padding="6"
BorderBrush="{ThemeResource CalendarSeperatorBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" MinHeight="35" />
</Grid.RowDefinitions>
<!-- Day number -->
<TextBlock x:Name="PART_HeaderDateDayText" FontSize="17" />
<!-- Extras -->
<StackPanel Grid.Column="1" HorizontalAlignment="Right" />
<!-- All-Multi Day Events -->
<ItemsControl
x:Name="PART_AllDayItemsControl"
Grid.Row="1"
Grid.ColumnSpan="2"
Margin="0,6">
<ItemsControl.ItemTemplateSelector>
<selectors:CustomAreaCalendarItemSelector>
<selectors:CustomAreaCalendarItemSelector.AllDayTemplate>
<DataTemplate x:DataType="data:CalendarItemViewModel">
<controls:CalendarItemControl
CalendarItem="{x:Bind}"
DisplayingDate="{Binding DataContext, ElementName=PART_AllDayItemsControl}"
IsCustomEventArea="True" />
</DataTemplate>
</selectors:CustomAreaCalendarItemSelector.AllDayTemplate>
<selectors:CustomAreaCalendarItemSelector.MultiDayTemplate>
<DataTemplate x:DataType="data:CalendarItemViewModel">
<controls:CalendarItemControl
CalendarItem="{x:Bind}"
DisplayingDate="{Binding DataContext, ElementName=PART_AllDayItemsControl}"
IsCustomEventArea="True" />
</DataTemplate>
</selectors:CustomAreaCalendarItemSelector.MultiDayTemplate>
</selectors:CustomAreaCalendarItemSelector>
</ItemsControl.ItemTemplateSelector>
<ItemsControl.ItemContainerTransitions>
<TransitionCollection>
<AddDeleteThemeTransition />
</TransitionCollection>
</ItemsControl.ItemContainerTransitions>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" Spacing="2" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="TodayOrNotStates">
<VisualState x:Name="NotTodayState" />
<VisualState x:Name="TodayState">
<VisualState.Setters>
<Setter Target="PART_IsTodayBorder.Visibility" Value="Visible" />
<Setter Target="PART_HeaderDateDayText.Foreground" Value="{ThemeResource SystemAccentColor}" />
<Setter Target="PART_HeaderDateDayText.FontWeight" Value="Semibold" />
<Setter Target="PART_ColumnHeaderText.FontWeight" Value="Semibold" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
<!-- Monthly data control for months -->
<ControlTemplate x:Key="MonthlyColumnControlTemplate" TargetType="controls:DayColumnControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="5" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Border for today indication. -->
<Border
x:Name="PART_IsTodayBorder"
Grid.Row="0"
Height="5"
Margin="2,0,2,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Background="{ThemeResource SystemAccentColor}"
CornerRadius="2"
Visibility="Collapsed" />
<!-- Border -->
<Grid
Grid.Row="0"
Grid.RowSpan="2"
BorderBrush="{ThemeResource CalendarSeperatorBrush}"
BorderThickness="1,1,0,1" />
<!-- Place where full day events go. -->
<Grid
x:Name="PART_DayDataAreaGrid"
Grid.Row="1"
Padding="6"
BorderBrush="{ThemeResource CalendarSeperatorBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" MinHeight="35" />
</Grid.RowDefinitions>
<!-- Extras -->
<StackPanel Grid.Column="1" HorizontalAlignment="Right" />
<!-- All events summary. -->
<ScrollViewer
Grid.Row="1"
Grid.ColumnSpan="2"
Margin="0,6"
Padding="0,0,16,0">
<ItemsControl x:Name="PART_AllDayItemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="data:CalendarItemViewModel">
<controls:CalendarItemControl
CalendarItem="{x:Bind}"
DisplayingDate="{Binding DataContext, ElementName=PART_AllDayItemsControl}"
IsCustomEventArea="True" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerTransitions>
<TransitionCollection>
<AddDeleteThemeTransition />
</TransitionCollection>
</ItemsControl.ItemContainerTransitions>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" Spacing="2" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="TodayOrNotStates">
<VisualState x:Name="NotTodayState" />
<VisualState x:Name="TodayState">
<VisualState.Setters>
<Setter Target="PART_IsTodayBorder.Visibility" Value="Visible" />
<Setter Target="PART_HeaderDateDayText.Foreground" Value="{ThemeResource SystemAccentColor}" />
<Setter Target="PART_HeaderDateDayText.FontWeight" Value="Semibold" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
<!-- Default style for DayColumnControl -->
<Style TargetType="controls:DayColumnControl">
<Setter Property="Template" Value="{StaticResource DailyColumnControlTemplate}" />
</Style>
</ResourceDictionary>
@@ -0,0 +1,11 @@
using Microsoft.UI.Xaml;
namespace Wino.Styles;
public sealed partial class WinoCalendarResources : ResourceDictionary
{
public WinoCalendarResources()
{
this.InitializeComponent();
}
}
@@ -0,0 +1,83 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Calendar.Controls"
xmlns:controls1="using:Wino.Mail.WinUI.Controls"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
<Style TargetType="controls:WinoCalendarTypeSelectorControl">
<Style.Setters>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:WinoCalendarTypeSelectorControl">
<CommandBar
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
DefaultLabelPosition="Right">
<CommandBar.PrimaryCommands>
<!-- Today -->
<AppBarButton
x:Name="PART_TodayButton"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
Label="Today">
<AppBarButton.Icon>
<controls1:WinoFontIcon Icon="CalendarToday" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarSeparator />
<!-- Day -->
<!-- TODO: Specific days -->
<AppBarToggleButton
x:Name="PART_DayToggle"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
Label="Day">
<AppBarToggleButton.Icon>
<controls1:WinoFontIcon Icon="CalendarDay" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<!-- Week -->
<!-- TODO: Work week -->
<AppBarToggleButton
x:Name="PART_WeekToggle"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
Label="Week">
<AppBarToggleButton.Icon>
<controls1:WinoFontIcon Icon="CalendarWeek" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<!-- Month -->
<AppBarToggleButton
x:Name="PART_MonthToggle"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
Label="Month">
<AppBarToggleButton.Icon>
<controls1:WinoFontIcon FontSize="44" Icon="CalendarMonth" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<!-- Year -->
<AppBarToggleButton
x:Name="PART_YearToggle"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
Label="Year">
<AppBarToggleButton.Icon>
<controls1:WinoFontIcon Icon="CalendarYear" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
</CommandBar.PrimaryCommands>
</CommandBar>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</ResourceDictionary>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Calendar.Controls">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="CalendarThemeResources.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="controls:WinoCalendarView">
<Setter Property="VisibleDateBackground" Value="{ThemeResource WinoCalendarViewVisibleDayBackgroundBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:WinoCalendarView">
<CalendarView
x:Name="PART_CalendarView"
BorderBrush="{ThemeResource WinoCalendarViewBorderBrush}"
CalendarItemCornerRadius="5"
CornerRadius="4"
DayItemMargin="0"
IsTodayHighlighted="False"
SelectionMode="Single">
<CalendarView.CalendarViewDayItemStyle>
<Style TargetType="CalendarViewDayItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CalendarViewDayItem">
<Grid>
<Border
x:Name="PART_DayViewItemBorder"
Margin="0,-1,0,-3"
CornerRadius="5">
<ContentPresenter />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</CalendarView.CalendarViewDayItemStyle>
</CalendarView>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -0,0 +1,19 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:controls="using:Wino.Calendar.Controls">
<!-- Background Timeline Canvas -->
<Style TargetType="controls:WinoDayTimelineCanvas">
<Style.Setters>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:WinoDayTimelineCanvas">
<canvas:CanvasControl x:Name="PART_InternalCanvas" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</ResourceDictionary>
@@ -0,0 +1,6 @@
using Wino.Calendar.ViewModels;
using Wino.Mail.WinUI;
namespace Wino.Mail.Views.Abstract;
public abstract class CalendarAppShellAbstract : BasePage<CalendarAppShellViewModel> { }
@@ -0,0 +1,6 @@
using Wino.Calendar.ViewModels;
using Wino.Mail.WinUI;
namespace Wino.Calendar.Views.Abstract;
public abstract class CalendarPageAbstract : BasePage<CalendarPageViewModel> { }
@@ -0,0 +1,5 @@
using Wino.Calendar.ViewModels;
namespace Wino.Mail.WinUI.Views.Abstract;
public abstract class EventDetailsPageAbstract : BasePage<EventDetailsPageViewModel> { }
@@ -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<AppShellViewModel>
public abstract class MailAppShellAbstract : BasePage<MailAppShellViewModel>
{
}
File diff suppressed because one or more lines are too long
@@ -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<CalendarDisplayTypeChangedMessage>
{
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<CalendarDisplayTypeChangedMessage>(this);
}
protected override void UnregisterRecipients()
{
base.UnregisterRecipients();
WeakReferenceMessenger.Default.Unregister<CalendarDisplayTypeChangedMessage>(this);
}
}
File diff suppressed because one or more lines are too long
@@ -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<ScrollToDateMessage>,
IRecipient<ScrollToHourMessage>,
IRecipient<GoNextDateRequestedMessage>,
IRecipient<GoPreviousDateRequestedMessage>
{
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;
}
}
@@ -0,0 +1,296 @@
<?xml version="1.0" encoding="utf-8" ?>
<abstract:EventDetailsPageAbstract
x:Class="Wino.Calendar.Views.EventDetailsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract"
xmlns:calendar="using:Wino.Core.Domain.Entities.Calendar"
xmlns:calendarHelpers="using:Wino.Calendar.Helpers"
xmlns:coreControls="using:Wino.Mail.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain"
xmlns:helpers="using:Wino.Helpers"
xmlns:local="using:Wino.Calendar.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Style="{StaticResource PageStyle}"
mc:Ignorable="d">
<Page.Resources>
<Style x:Key="ActionBarElementContainerStackStyle" TargetType="StackPanel">
<Setter Property="Spacing" Value="6" />
<Setter Property="Padding" Value="10,0,4,0" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Orientation" Value="Horizontal" />
</Style>
<Style TargetType="AppBarElementContainer">
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style x:Key="EventDetailsPanelGridStyle" TargetType="Grid">
<Setter Property="Padding" Value="0,12" />
<Setter Property="Margin" Value="12,0" />
</Style>
<Style
x:Key="EventDetailsPanelTitleStyle"
BasedOn="{StaticResource SubtitleTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Margin" Value="0,0,0,20" />
</Style>
</Page.Resources>
<Grid Padding="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Action Bar -->
<Border
VerticalAlignment="Top"
Background="{ThemeResource WinoContentZoneBackgroud}"
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="7">
<CommandBar
HorizontalAlignment="Left"
Background="Transparent"
DefaultLabelPosition="Right"
IsSticky="True"
OverflowButtonVisibility="Auto">
<AppBarToggleButton
x:Name="ReadOnlyToggle"
Content="Read-only event"
IsChecked="True" />
<AppBarButton Label="{x:Bind domain:Translator.Buttons_Save}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="Save" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton Label="{x:Bind domain:Translator.Buttons_Delete}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="Delete" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarSeparator />
<!-- Join Online -->
<AppBarButton Label="Join Online">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="EventJoinOnline" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarSeparator />
<!-- Join Options -->
<AppBarButton Label="Accept">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Foreground="#527257" Icon="EventAccept" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton Label="Tentative">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Foreground="#805682" Icon="EventTentative" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton Label="Decline">
<AppBarButton.Icon>
<PathIcon Data="F1 M 10.253906 9.375 L 16.064453 15.185547 C 16.18815 15.309245 16.25 15.455729 16.25 15.625 C 16.25 15.794271 16.18815 15.940756 16.064453 16.064453 C 15.940754 16.188152 15.79427 16.25 15.625 16.25 C 15.455729 16.25 15.309244 16.188152 15.185547 16.064453 L 9.375 10.253906 L 3.564453 16.064453 C 3.440755 16.188152 3.294271 16.25 3.125 16.25 C 2.955729 16.25 2.809245 16.188152 2.685547 16.064453 C 2.561849 15.940756 2.5 15.794271 2.5 15.625 C 2.5 15.455729 2.561849 15.309245 2.685547 15.185547 L 8.496094 9.375 L 2.685547 3.564453 C 2.561849 3.440756 2.5 3.294271 2.5 3.125 C 2.5 2.95573 2.561849 2.809246 2.685547 2.685547 C 2.809245 2.56185 2.955729 2.5 3.125 2.5 C 3.294271 2.5 3.440755 2.56185 3.564453 2.685547 L 9.375 8.496094 L 15.185547 2.685547 C 15.309244 2.56185 15.455729 2.5 15.625 2.5 C 15.79427 2.5 15.940754 2.56185 16.064453 2.685547 C 16.18815 2.809246 16.25 2.95573 16.25 3.125 C 16.25 3.294271 16.18815 3.440756 16.064453 3.564453 Z " Foreground="#d94b4e" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton Label="Respond">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Foreground="#805682" Icon="EventRespond" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarSeparator />
<!-- Show as -->
<AppBarElementContainer>
<StackPanel Style="{StaticResource ActionBarElementContainerStackStyle}">
<TextBlock VerticalAlignment="Center" Text="Show as" />
<ComboBox Width="150" />
</StackPanel>
</AppBarElementContainer>
<!-- Reminder -->
<AppBarElementContainer>
<StackPanel Style="{StaticResource ActionBarElementContainerStackStyle}">
<coreControls:WinoFontIcon FontSize="16" Icon="Reminder" />
<TextBlock VerticalAlignment="Center" Text="Reminder" />
<ComboBox Width="150" />
</StackPanel>
</AppBarElementContainer>
<AppBarSeparator />
<!-- Edit Series -->
<AppBarButton Label="Edit Series">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="EventEditSeries" />
</AppBarButton.Icon>
</AppBarButton>
</CommandBar>
</Border>
<!-- Event details -->
<ScrollViewer Grid.Row="1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Details -->
<Grid x:Name="DetailsGrid" Style="{StaticResource EventDetailsPanelGridStyle}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="Details" />
<Grid Grid.Row="1">
<!-- Read-Only Event -->
<Grid
x:Name="ReadOnlyDetailsGrid"
RowSpacing="6"
Visibility="{x:Bind ReadOnlyToggle.IsChecked, Mode=OneWay}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="16" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Title -->
<TextBlock
Style="{StaticResource SubheaderTextBlockStyle}"
Text="{x:Bind ViewModel.CurrentEvent.Title, Mode=OneWay}"
TextWrapping="Wrap" />
<!-- Date and Duration -->
<TextBlock
Grid.Row="2"
Text="{x:Bind calendarHelpers:CalendarXamlHelpers.GetEventDetailsDateString(ViewModel.CurrentEvent, ViewModel.CurrentSettings), Mode=OneWay}"
TextWrapping="Wrap" />
<!-- Recurrence Info -->
<Grid
Grid.Row="3"
ColumnSpacing="6"
Visibility="{x:Bind ViewModel.CurrentEvent.IsRecurringEvent}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<coreControls:WinoFontIcon FontSize="14" Icon="CalendarEventRepeat" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="{x:Bind calendarHelpers:CalendarXamlHelpers.GetRecurrenceString(ViewModel.CurrentEvent), Mode=OneWay}"
TextWrapping="Wrap" />
</Grid>
</Grid>
<!-- Editable Event -->
<Grid Visibility="{x:Bind helpers:XamlHelpers.ReverseVisibilityConverter(ReadOnlyDetailsGrid.Visibility), Mode=OneWay}">
<TextBlock Text="editing" />
</Grid>
</Grid>
</Grid>
<!-- People -->
<Grid
x:Name="PeopleGrid"
Grid.Column="1"
Style="{StaticResource EventDetailsPanelGridStyle}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="People" />
<Grid Grid.Row="1" RowSpacing="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<AutoSuggestBox
Margin="6,0"
BorderThickness="0"
PlaceholderText="Invite someone" />
<ListView
Grid.Row="1"
IsItemClickEnabled="True"
ItemsSource="{x:Bind ViewModel.CurrentEvent.Attendees, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="calendar:CalendarEventAttendee">
<Grid Margin="0,6" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<PersonPicture
Width="40"
Height="40"
DisplayName="{x:Bind Name}" />
<!-- TODO: Organizer -->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock FontWeight="SemiBold" Text="{x:Bind Name}" />
<TextBlock
Grid.Row="1"
FontSize="13"
Text="{x:Bind Email}" />
</Grid>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Grid>
<!-- Attachments -->
<Grid
x:Name="AttachmentsGrid"
Grid.Column="2"
Style="{StaticResource EventDetailsPanelGridStyle}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="Attachments" />
</Grid>
</Grid>
</ScrollViewer>
</Grid>
</abstract:EventDetailsPageAbstract>
@@ -0,0 +1,11 @@
using Wino.Mail.WinUI.Views.Abstract;
namespace Wino.Calendar.Views;
public sealed partial class EventDetailsPage : EventDetailsPageAbstract
{
public EventDetailsPage()
{
this.InitializeComponent();
}
}
@@ -1,5 +1,5 @@
<abstract:ComposePageAbstract
x:Class="Wino.Views.ComposePage"
x:Class="Wino.Views.Mail.ComposePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:abstract="using:Wino.Views.Abstract"
@@ -439,31 +439,34 @@
</ToggleSplitButton>
</AppBarElementContainer>
<AppBarElementContainer>
<muxc:ToggleSplitButton x:Name="SignatureToggleButton"
Margin="8,0,0,0"
IsChecked="{x:Bind ViewModel.IsSmimeSignatureEnabled, Mode=TwoWay}"
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_EnableSmimeSignature}"
IsEnabled="{x:Bind ViewModel.AreCertificatesAvailable, Mode=OneWay}">
<muxc:ToggleSplitButton
x:Name="SignatureToggleButton"
Margin="8,0,0,0"
IsChecked="{x:Bind ViewModel.IsSmimeSignatureEnabled, Mode=TwoWay}"
IsEnabled="{x:Bind ViewModel.AreCertificatesAvailable, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_EnableSmimeSignature}">
<muxc:ToggleSplitButton.Content>
<Viewbox Width="16" Height="16" Margin="0,0,4,0">
<PathIcon
Data="M15 18c.835.629 1.875 1.001 3 1.001a4.978 4.978 0 0 0 3-.999v3.246a.75.75 0 0 1-1.09.67l-.09-.055L18 20.591l-1.82 1.272a.75.75 0 0 1-1.172-.51l-.007-.105L15 18.001Zm4.25-14.996a2.75 2.75 0 0 1 2.745 2.582l.005.168.001 5.246a5.027 5.027 0 0 0-1.5-1.331L20.5 5.754a1.25 1.25 0 0 0-1.122-1.244l-.128-.006H4.75a1.25 1.25 0 0 0-1.244 1.122l-.006.128v9.5c0 .647.492 1.18 1.122 1.243l.128.007h8.92c.1.172.21.338.33.496v1.004H4.75a2.75 2.75 0 0 1-2.745-2.583L2 15.254v-9.5a2.75 2.75 0 0 1 2.582-2.745l.168-.005h14.5ZM18 10A4 4 0 1 1 18 18 4 4 0 0 1 18 10Zm-6.75 2.5a.75.75 0 0 1 .102 1.493L11.25 14h-4.5a.75.75 0 0 1-.102-1.493l.102-.007h4.5Zm6-5.5a.75.75 0 0 1 .102 1.493l-.102.007H6.75a.75.75 0 0 1-.102-1.493L6.75 7h10.5Z" />
<Viewbox
Width="16"
Height="16"
Margin="0,0,4,0">
<PathIcon Data="M15 18c.835.629 1.875 1.001 3 1.001a4.978 4.978 0 0 0 3-.999v3.246a.75.75 0 0 1-1.09.67l-.09-.055L18 20.591l-1.82 1.272a.75.75 0 0 1-1.172-.51l-.007-.105L15 18.001Zm4.25-14.996a2.75 2.75 0 0 1 2.745 2.582l.005.168.001 5.246a5.027 5.027 0 0 0-1.5-1.331L20.5 5.754a1.25 1.25 0 0 0-1.122-1.244l-.128-.006H4.75a1.25 1.25 0 0 0-1.244 1.122l-.006.128v9.5c0 .647.492 1.18 1.122 1.243l.128.007h8.92c.1.172.21.338.33.496v1.004H4.75a2.75 2.75 0 0 1-2.745-2.583L2 15.254v-9.5a2.75 2.75 0 0 1 2.582-2.745l.168-.005h14.5ZM18 10A4 4 0 1 1 18 18 4 4 0 0 1 18 10Zm-6.75 2.5a.75.75 0 0 1 .102 1.493L11.25 14h-4.5a.75.75 0 0 1-.102-1.493l.102-.007h4.5Zm6-5.5a.75.75 0 0 1 .102 1.493l-.102.007H6.75a.75.75 0 0 1-.102-1.493L6.75 7h10.5Z" />
</Viewbox>
</muxc:ToggleSplitButton.Content>
<muxc:ToggleSplitButton.Flyout>
<Flyout Placement="Bottom">
<ListView
Width="320"
ItemsSource="{x:Bind ViewModel.AvailableCertificates, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedSigningCertificate, Mode=TwoWay}"
Width="320">
SelectedItem="{x:Bind ViewModel.SelectedSigningCertificate, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="x509:X509Certificate2" xmlns:x509="using:System.Security.Cryptography.X509Certificates">
<StackPanel Orientation="Vertical" Padding="8,4">
<TextBlock Text="{x:Bind Subject}" FontWeight="SemiBold" />
<TextBlock Text="{x:Bind Issuer}" FontSize="12" />
<DataTemplate xmlns:x509="using:System.Security.Cryptography.X509Certificates" x:DataType="x509:X509Certificate2">
<StackPanel Padding="8,4" Orientation="Vertical">
<TextBlock FontWeight="SemiBold" Text="{x:Bind Subject}" />
<TextBlock FontSize="12" Text="{x:Bind Issuer}" />
<TextBlock>
<Run Text="{x:Bind domain:Translator.Composer_CertificateExpires}"/>
<Run Text="{x:Bind NotAfter, Mode=OneWay}"/>
<Run Text="{x:Bind domain:Translator.Composer_CertificateExpires}" />
<Run Text="{x:Bind NotAfter, Mode=OneWay}" />
</TextBlock>
</StackPanel>
</DataTemplate>
@@ -474,14 +477,17 @@
</muxc:ToggleSplitButton>
</AppBarElementContainer>
<AppBarElementContainer>
<ToggleButton x:Name="EncryptionToggleButton"
Margin="8,0,0,0"
IsChecked="{x:Bind ViewModel.IsSmimeEncryptionEnabled, Mode=TwoWay}"
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_EnableSmimeEncryption}">
<ToggleButton
x:Name="EncryptionToggleButton"
Margin="8,0,0,0"
IsChecked="{x:Bind ViewModel.IsSmimeEncryptionEnabled, Mode=TwoWay}"
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_EnableSmimeEncryption}">
<ToggleButton.Content>
<Viewbox Width="16" Height="16" Margin="0,0,4,0">
<PathIcon
Data="M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z" />
<Viewbox
Width="16"
Height="16"
Margin="0,0,4,0">
<PathIcon Data="M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z" />
</Viewbox>
</ToggleButton.Content>
</ToggleButton>
@@ -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<CreateNewComposeMailRequested>,
@@ -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;
@@ -1,5 +1,5 @@
<abstract:IdlePageAbstract
x:Class="Wino.Views.IdlePage"
x:Class="Wino.Views.Mail.IdlePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:abstract="using:Wino.Views.Abstract"
@@ -1,6 +1,6 @@
using Wino.Views.Abstract;
namespace Wino.Views;
namespace Wino.Views.Mail;
public sealed partial class IdlePage : IdlePageAbstract
{
@@ -1,5 +1,5 @@
<abstract:MailListPageAbstract
x:Class="Wino.Views.MailListPage"
x:Class="Wino.Views.Mail.MailListPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:abstract="using:Wino.Views.Abstract"
@@ -30,7 +30,7 @@ using Wino.MenuFlyouts.Context;
using Wino.Messaging.Client.Mails;
using Wino.Views.Abstract;
namespace Wino.Views;
namespace Wino.Views.Mail;
public sealed partial class MailListPage : MailListPageAbstract,
IRecipient<ClearMailSelectionsRequested>,
@@ -1,5 +1,5 @@
<abstract:MailRenderingPageAbstract
x:Class="Wino.Views.MailRenderingPage"
x:Class="Wino.Views.Mail.MailRenderingPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:abstract="using:Wino.Views.Abstract"
@@ -209,46 +209,53 @@
</HyperlinkButton>
<!-- S/MIME Signed Email Indicator -->
<!-- S/MIME Signed Email Indicator -->
<HyperlinkButton
Grid.Column="2"
VerticalAlignment="Center"
Margin="0,0,0,4"
VerticalAlignment="Center"
Command="{x:Bind ViewModel.ShowSmimeSigningCertificateInfoCommand}"
ToolTipService.ToolTip="{x:Bind domain:Translator.SmimeSignedTooltip}"
Visibility="{x:Bind ViewModel.IsSmimeSigned, Mode=OneWay}"
Command="{x:Bind ViewModel.ShowSmimeSigningCertificateInfoCommand}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Width="Auto" Height="Auto">
<Viewbox Width="16" Height="16" Margin="0,0,4,0">
<PathIcon
Data="M15 18c.835.629 1.875 1.001 3 1.001a4.978 4.978 0 0 0 3-.999v3.246a.75.75 0 0 1-1.09.67l-.09-.055L18 20.591l-1.82 1.272a.75.75 0 0 1-1.172-.51l-.007-.105L15 18.001Zm4.25-14.996a2.75 2.75 0 0 1 2.745 2.582l.005.168.001 5.246a5.027 5.027 0 0 0-1.5-1.331L20.5 5.754a1.25 1.25 0 0 0-1.122-1.244l-.128-.006H4.75a1.25 1.25 0 0 0-1.244 1.122l-.006.128v9.5c0 .647.492 1.18 1.122 1.243l.128.007h8.92c.1.172.21.338.33.496v1.004H4.75a2.75 2.75 0 0 1-2.745-2.583L2 15.254v-9.5a2.75 2.75 0 0 1 2.582-2.745l.168-.005h14.5ZM18 10A4 4 0 1 1 18 18 4 4 0 0 1 18 10Zm-6.75 2.5a.75.75 0 0 1 .102 1.493L11.25 14h-4.5a.75.75 0 0 1-.102-1.493l.102-.007h4.5Zm6-5.5a.75.75 0 0 1 .102 1.493l-.102.007H6.75a.75.75 0 0 1-.102-1.493L6.75 7h10.5Z" />
Visibility="{x:Bind ViewModel.IsSmimeSigned, Mode=OneWay}">
<Grid
Width="Auto"
Height="Auto"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Viewbox
Width="16"
Height="16"
Margin="0,0,4,0">
<PathIcon Data="M15 18c.835.629 1.875 1.001 3 1.001a4.978 4.978 0 0 0 3-.999v3.246a.75.75 0 0 1-1.09.67l-.09-.055L18 20.591l-1.82 1.272a.75.75 0 0 1-1.172-.51l-.007-.105L15 18.001Zm4.25-14.996a2.75 2.75 0 0 1 2.745 2.582l.005.168.001 5.246a5.027 5.027 0 0 0-1.5-1.331L20.5 5.754a1.25 1.25 0 0 0-1.122-1.244l-.128-.006H4.75a1.25 1.25 0 0 0-1.244 1.122l-.006.128v9.5c0 .647.492 1.18 1.122 1.243l.128.007h8.92c.1.172.21.338.33.496v1.004H4.75a2.75 2.75 0 0 1-2.745-2.583L2 15.254v-9.5a2.75 2.75 0 0 1 2.582-2.745l.168-.005h14.5ZM18 10A4 4 0 1 1 18 18 4 4 0 0 1 18 10Zm-6.75 2.5a.75.75 0 0 1 .102 1.493L11.25 14h-4.5a.75.75 0 0 1-.102-1.493l.102-.007h4.5Zm6-5.5a.75.75 0 0 1 .102 1.493l-.102.007H6.75a.75.75 0 0 1-.102-1.493L6.75 7h10.5Z" />
</Viewbox>
<muxc:InfoBadge
Visibility="{x:Bind ViewModel.SmimeSignaturesInvalid, Mode=OneWay}"
Width="8"
Height="8"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Style="{ThemeResource CautionIconInfoBadgeStyle}"/>
Style="{ThemeResource CautionIconInfoBadgeStyle}"
Visibility="{x:Bind ViewModel.SmimeSignaturesInvalid, Mode=OneWay}" />
<muxc:InfoBadge
Visibility="{x:Bind ViewModel.SmimeSignaturesValid, Mode=OneWay}"
Width="8"
Height="8"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Style="{ThemeResource SuccessIconInfoBadgeStyle}"/>
Style="{ThemeResource SuccessIconInfoBadgeStyle}"
Visibility="{x:Bind ViewModel.SmimeSignaturesValid, Mode=OneWay}" />
</Grid>
</HyperlinkButton>
<!-- S/MIME Encrypted Email Indicator -->
<Viewbox Width="16" Height="16"
Grid.Column="3"
VerticalAlignment="Center"
Margin="0,0,0,4"
ToolTipService.ToolTip="{x:Bind domain:Translator.SmimeEncryptedTooltip}"
Visibility="{x:Bind ViewModel.IsSmimeEncrypted, Mode=OneWay}">
<PathIcon
Data="M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z" />
</Viewbox>
<!-- S/MIME Encrypted Email Indicator -->
<Viewbox
Grid.Column="3"
Width="16"
Height="16"
Margin="0,0,0,4"
VerticalAlignment="Center"
ToolTipService.ToolTip="{x:Bind domain:Translator.SmimeEncryptedTooltip}"
Visibility="{x:Bind ViewModel.IsSmimeEncrypted, Mode=OneWay}">
<PathIcon Data="M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z" />
</Viewbox>
</Grid>
<CommandBar
x:Name="RendererBar"
@@ -21,7 +21,7 @@ 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 MailRenderingPage : MailRenderingPageAbstract,
IRecipient<HtmlRenderingRequested>,
+45 -1
View File
@@ -90,6 +90,15 @@
<ItemGroup>
<None Remove="Controls\ListView\WinoListViewStyles.xaml" />
<None Remove="ShellWindow.xaml" />
<None Remove="Styles\CalendarThemeResources.xaml" />
<None Remove="Styles\DayHeaderControl.xaml" />
<None Remove="Styles\WinoCalendarResources.xaml" />
<None Remove="Styles\WinoCalendarTypeSelectorControl.xaml" />
<None Remove="Styles\WinoCalendarView.xaml" />
<None Remove="Styles\WinoDayTimelineCanvas.xaml" />
<None Remove="Views\Calendar\CalendarAppShell.xaml" />
<None Remove="Views\Calendar\CalendarPage.xaml" />
<None Remove="Views\Calendar\EventDetailsPage.xaml" />
<None Remove="Views\Settings\ContactsPage.xaml" />
</ItemGroup>
@@ -152,6 +161,7 @@
<PackageReference Include="H.NotifyIcon.WinUI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Calendar.ViewModels\Wino.Calendar.ViewModels.csproj" />
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
<ProjectReference Include="..\Wino.Core.ViewModels\Wino.Core.ViewModels.csproj" />
<ProjectReference Include="..\Wino.Mail.ViewModels\Wino.Mail.ViewModels.csproj" />
@@ -173,7 +183,7 @@
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="AppShell.xaml">
<Page Update="MailAppShell.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\MailItemDisplayInformationControl.xaml">
@@ -279,6 +289,40 @@
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<Folder Include="Views\Mail\" />
</ItemGroup>
<ItemGroup>
<Page Update="Styles\CalendarThemeResources.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\DayHeaderControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\WinoCalendarResources.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\WinoCalendarTypeSelectorControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\WinoCalendarView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\WinoDayTimelineCanvas.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\Calendar\CalendarAppShell.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Views\Calendar\CalendarPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Views\Calendar\EventDetailsPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
@@ -13,4 +13,4 @@ public record NewMailSynchronizationRequested(MailSynchronizationOptions Options
/// Triggers a new calendar synchronization if possible.
/// </summary>
/// <param name="Options">Options for synchronization.</param>
public record NewCalendarSynchronizationRequested(CalendarSynchronizationOptions Options) : IClientMessage;
public record NewCalendarSynchronizationRequested(CalendarSynchronizationOptions Options) : IClientMessage, IUIMessage;
+6 -6
View File
@@ -1,6 +1,7 @@
using System.IO;
using System.Threading.Tasks;
using SQLite;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
@@ -43,11 +44,6 @@ public class DatabaseService : IDatabaseService
private async Task CreateTablesAsync()
{
//typeof(AccountCalendar),
// typeof(CalendarEventAttendee),
// typeof(CalendarItem),
// typeof(Reminder),
await Task.WhenAll(
Connection.CreateTableAsync<MailCopy>(),
Connection.CreateTableAsync<MailItemFolder>(),
@@ -59,7 +55,11 @@ public class DatabaseService : IDatabaseService
Connection.CreateTableAsync<MailAccountPreferences>(),
Connection.CreateTableAsync<MailAccountAlias>(),
Connection.CreateTableAsync<Thumbnail>(),
Connection.CreateTableAsync<KeyboardShortcut>()
Connection.CreateTableAsync<KeyboardShortcut>(),
Connection.CreateTableAsync<AccountCalendar>(),
Connection.CreateTableAsync<CalendarEventAttendee>(),
Connection.CreateTableAsync<CalendarItem>(),
Connection.CreateTableAsync<Reminder>()
);
}
}
+5 -1
View File
@@ -6,7 +6,6 @@
</Configurations>
<Folder Name="/Solution Items/" Id="8ec462fd-d22e-90a8-e5ce-7e832ba40c5d">
<File Path=".editorconfig" />
<File Path=".github/copilot-instructions.md" />
<File Path="Directory.Build.Props" />
<File Path="Directory.Packages.props" />
<File Path="nuget.config" />
@@ -18,6 +17,11 @@
<Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" />
</Project>
<Project Path="Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj">
<Platform Solution="*|arm64" Project="arm64" />
<Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" />
</Project>
<Project Path="Wino.Core.Domain/Wino.Core.Domain.csproj">
<BuildDependency Project="Wino.SourceGenerators/Wino.SourceGenerators.csproj" />
<Platform Solution="*|arm64" Project="arm64" />